MailFloearly access
← Blog

· 5 min read

Push, not just poll: real-time notifications for inbound email

Inbound email on MailFlo has always worked the same way: point an MX record at us, and every message that lands gets parsed and forwarded to your webhook. That's still true today. What changed is that it's no longer the only way to find out mail arrived.

The webhook's blind spot

A webhook assumes something you can usually take for granted but not always: a public HTTPS endpoint that MailFlo can reach and POST to. That's a fine assumption for a production API. It's a bad one for a local dev environment behind NAT, a desktop or CLI tool with no server component, a backend sitting entirely inside a private network, or a support console someone wants to update live without wiring up infrastructure just to receive a callback. Those consumers were stuck polling GET /api/v1/inbound on a timer, trading latency for simplicity.

What's new: a push channel, alongside the webhook

Every project with inbound enabled can now open a WebSocket connection and get notified the instant a message lands — no public endpoint required, no polling loop, no added latency waiting for the next tick. It authenticates with the same project-scoped API key your REST calls already use, and each project gets its own isolated connection pool: a socket can only ever be routed to the project whose key opened it.

This is additive, not a migration. Your webhook keeps firing exactly as before — retried with backoff, then dead-lettered if your endpoint is down. The push channel is a second delivery path off the same event, for the cases a webhook can't reach.

How a message actually reaches you

The push itself is deliberately thin — an id, the sender, and the subject, not the full message. The body, headers and attachments live behind the same authenticated REST endpoint the webhook payload already points back to, so there's exactly one source of truth regardless of which path told you a message existed:

Mail arrives

you@yourdomain.com — MX points at MailFlo

Parsed & stored

One durable record: sender, subject, body, attachments

New: push

Thin push over WebSocket

id, from, subject — no body, sent the instant mail lands

You fetch the body

GET /api/v1/inbound/:id — only when you actually need it

Existing: webhook

Webhook queue

Retries with backoff, then a dead-letter queue

POST to your endpoint

Full JSON payload delivered in one call

Why a thin push instead of pushing the whole email

Trying it

Turn on inbound for a project, grab its API key from the console, and connect:

import WebSocket from "ws";

const ws = new WebSocket("wss://YOUR_INBOUND_HOST/v1/inbound/stream", {
  headers: { Authorization: `Bearer ${process.env.MAILFLO_API_KEY}` },
});

ws.on("message", async (raw) => {
  // Thin push: just enough to decide whether you care.
  const { id, from_email, subject } = JSON.parse(raw.toString());

  const res = await fetch(`https://console.mailflo.dev/api/v1/inbound/${id}`, {
    headers: { Authorization: `Bearer ${process.env.MAILFLO_API_KEY}` },
  });
  const { data: email } = await res.json();

  console.log(email.subject, email.text); // full body, fetched on demand
});

Your webhook URL, if you have one set, keeps working unchanged — the two paths run side by side, and you can lean on whichever one fits a given consumer.

Want inbound email on your own domain?

Apply for early access and get a project set up in minutes.

Request an invite