Blog

/

Wiring your product into the inbox: REST API and webhooks for email automation

The most useful question to ask about any email platform is not “how do I read email in the UI” — it is “how do my service code and my product get notified, and what can they do in response.” This post is a tour of the Flowmails API surface, the webhook model, and a worked example.

The shape of the REST API

The API is REST over HTTPS. Every action the dashboard can perform has an equivalent endpoint, and the responses use ordinary JSON. Authentication is a bearer token issued per domain, with rate limits and an audit trail scoped to that token.

The endpoints cluster into four groups:

  • Messages. GET /messages, GET /messages/:id, POST /messages (send), and DELETE /messages/:id. List endpoints support cursor pagination and a small set of stable filters: by folder, by tag, by date range, by sender.
  • Routing rules. GET /rules, POST /rules, PATCH /rules/:id. Rule shape is JSON, not a UI DSL, so you can manage routing entirely from code if that is how your team prefers to work.
  • API keys. GET /keys, POST /keys, DELETE /keys/:id. Key creation returns the full key exactly once; subsequent reads only expose the prefix.
  • Webhooks. POST /webhooks, GET /webhooks/:id/deliveries. Subscribe to events, list recent deliveries, replay individual ones.

Webhooks vs polling, in one sentence

Use webhooks for “tell me when something happens” and polling for “let me search your history.” If you are tempted to poll /messages?since=... on a tight loop, you almost certainly want a webhook subscription instead — the latency is lower, the bill is lower, and the retry semantics are handled for you.

Webhook delivery, the long version

A webhook delivery is a POST to your endpoint with a JSON body describing the event. The body includes the message ID, the routing decision, a short-lived signed URL for the full message body, and a delivery ID you can use to deduplicate.

If your endpoint returns a non-2xx status, the delivery is retried with exponential backoff up to a configurable maximum (default 24 hours, 6 attempts). After the maximum, the delivery lands in a dead-letter log in D1 and is exposed via GET /webhooks/:id/deliveries so you can replay it manually.

Every delivery is signed with an HMAC of the body using your webhook secret. Verify the signature on your side before trusting the payload — the rest of the contract is on you.

A worked example: inbound to ticket in 30 lines

Imagine you want every inbound message to support@yourdomain.com to create a ticket in your issue tracker. With a webhook subscription, the receiving service looks roughly like this:

import { createHmac, timingSafeEqual } from "node:crypto";

const WEBHOOK_SECRET = process.env.FLOWMAILS_WEBHOOK_SECRET!;

export async function handle(request: Request) {
  const body = await request.text();
  const signature = request.headers.get("x-flowmails-signature") ?? "";

  const expected = createHmac("sha256", WEBHOOK_SECRET)
    .update(body)
    .digest("hex");
  if (
    !timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  ) {
    return new Response("bad signature", { status: 401 });
  }

  const event = JSON.parse(body);
  if (event.type !== "message.received") {
    return new Response("ignored", { status: 200 });
  }

  // Hand off to your ticketing system here.
  await createTicket({
    from: event.message.from,
    subject: event.message.subject,
    body_url: event.message.body_url,
    message_id: event.message.id,
  });

  return new Response("ok", { status: 200 });
}

That is the whole integration: one endpoint, one signature check, one outbound call to the system of record. From there it scales linearly with whatever your ticketing system can absorb.

What to read next

The runtime tour covers the parts of the pipeline that fire this webhook in the first place. If you are evaluating the platform as a whole, the pricing breakdown is where the rate-limit and message-volume numbers come from.

Try the API in your own Cloudflare account.

Start free