All posts
    Guides

    Receive Email in Real Time Without Running a Webhook Server

    The Devmailr Team5 min read

    Webhooks are the usual way to receive email in real time — but they need a public URL you control, which a CI runner, a laptop behind NAT, or a quick script doesn’t have. Here’s how to receive inbound email as it arrives over Server-Sent Events instead: one HTTP connection, no server to host.

    The webhook problem

    To use a webhook you must expose an HTTPS endpoint the service can POST to. Fine for a deployed backend — painful when:

    • You’re in CI — the runner is ephemeral and not publicly reachable.
    • You’re developing locally — you’d need ngrok or a tunnel.
    • You’re writing a quick script or agent — standing up a web server is overkill.

    The SSE alternative

    Server-Sent Events flips it around: instead of the service calling you, you open one long-lived GET request and it streams events down as they happen. Plain HTTP, works through any firewall that allows outbound HTTPS, no public URL. With Devmailr, open the stream for a mailbox:

    bash
    curl -N "https://api.devmailr.app/api/mailboxes/$ID/stream" \
      -H "Authorization: Bearer $DEVMAILR_API_KEY"

    Each inbound email is pushed as an event:

    event stream
    event: email.received
    data: {"_id":"...","from":"noreply@stripe.com","subject":"Your code","extractedCodes":["482913"]}

    Consuming it in Node

    javascript
    const res = await fetch(`${BASE}/mailboxes/${inbox._id}/stream`, {
      headers: { Authorization: `Bearer ${process.env.DEVMAILR_API_KEY}` },
    })
    
    const reader = res.body.getReader()
    const decoder = new TextDecoder()
    let buffer = ''
    
    while (true) {
      const { value, done } = await reader.read()
      if (done) break
      buffer += decoder.decode(value, { stream: true })
    
      let idx
      while ((idx = buffer.indexOf('\n\n')) !== -1) {
        const frame = buffer.slice(0, idx)
        buffer = buffer.slice(idx + 2)
        const dataLine = frame.split('\n').find((l) => l.startsWith('data:'))
        if (!dataLine) continue // skip ':' heartbeat comments
        const email = JSON.parse(dataLine.slice(5).trim())
        console.log('Got email from', email.from, '— code', email.extractedCodes?.[0])
      }
    }

    Filtering

    Narrow the stream with the same filters as the list and wait endpoints:

    bash
    # only emails from your app that carry a code
    curl -N "$BASE/mailboxes/$ID/stream?from=noreply@myapp.com&has_code=true" -H "$AUTH"

    When to use which

    • SSE — CI, local dev, scripts, agents: anything that can’t host a URL. One connection, no infra.
    • Webhooks — deployed backends that want push delivery to a stable endpoint, with retries.
    • /wait — when you want exactly one email and then to move on (most tests).

    Keep-alive: The stream sends a : heartbeat comment every ~25s to hold the connection open — ignore any frame without a data: line, and reconnect if it drops.

    Related reading

    Try it in two minutes

    Create a disposable prefix@devmailr.app inbox over the API, send mail to it from your app, and read it in your tests. Free plan, no card required.