Blog

/

Wiring your product into the inbox: REST API and the durable event log

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 event log, and a worked example.

The shape of the REST API

The API is REST over HTTPS 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 public surface has two halves, and it is worth being precise about which is which:

  • Sending — live. POST /api/v1/messages on sdk.flowmails.net. One endpoint: a from, a to-list, a subject, a body. It returns { id, status } and the message lands in your D1 outbox. The npm SDK wraps it.
  • Reading — v0.2 roadmap. GET /api/agent/events and POST /api/agent/waits. The log records what arrived — which mailbox, which sender, which message id, the subject line — and you read it with a cursor. The wait lets you ask “has the verification mail arrived yet?” and get an answer now instead of blocking a request for twenty minutes. The platform-side endpoints are built; the piece that makes them useful is the receive worker reporting each inbound message, which is still upstream work in the worker.

What is not in that list is as important as what is. There is no endpoint to read a message body over REST, no endpoint to manage routing rules, and no endpoint to manage API keys — those are dashboard surfaces, and the data behind them is stored as ordinary rows. So the integration reads them where it can and calls the one send endpoint where it must.

Polling, but not the way you were expecting

The usual reason to avoid polling is latency and cost. Both apply here, but the fix is not a webhook subscription — it is that the inbox is a table you can read. Your service already has a connection pool, a query planner, and a backoff strategy for everything else it does; the inbox is one more table, and reading it is one more query.

That is what works today, with no platform surface at all beyond the send endpoint. When you would rather not poll at all, the durable wait is the v0.2 answer to that: open one against an address, and the platform resolves it the moment a message for that address lands. The wait is state on the platform, not a suspended request — a request that hung on the edge would die long before a verification mail arrived.

Reading the log, the long version

The event log is append-only and read with a cursor. The cursor is an event id, never a timestamp — two events can land in the same millisecond, and a timestamp cursor would silently skip one of them. A cursor the platform does not recognise is an error, not an empty page: an agent that read “no new events” would quietly stop seeing mail.

An event carries the routing facts — the message id, the recipient, the sender, the event type — plus the subject line, so an agent can filter by topic before fetching anything. There is no body, and that is structural rather than a convention: the table has no column for a body or an attachment, so the message itself cannot reach the log even by accident. An agent that wants the body fetches it from the mail API under its own scopes.

Re-reading the same event is a no-op. The log has a unique key on (account, message, event type), so a worker that retries after a timeout gets the same event id back rather than a second copy.

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. Today that is a query against the inbox table — and this is a plain read against it:

// The inbox is a table in the Flowmails D1 database.
// This is a normal read against it.
const { results } = await env.DB.prepare(
  `SELECT id, from_email, subject, text, create_time
     FROM email
    WHERE to_email = ?1 AND type = 'receive' AND status = 0
    ORDER BY id DESC
    LIMIT 50`,
)
  .bind("support@yourdomain.com")
  .all();

for (const message of results) {
  // Hand off to your ticketing system here.
  await createTicket({
    from: message.from_email,
    subject: message.subject,
    body: message.text,
    message_id: String(message.id),
  });
}

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

Note what is not in that snippet. There is no webhook endpoint of your own to run, no signature to verify, no delivery log to reconcile. The message is one row, and reading it is a query you already know how to make.

When a wait beats a poll

A query loop is right when you want to keep processing mail. A durable wait is the v0.2 answer when you are blocked on one specific message — a signup flow waiting for the verification mail, a test waiting for a receipt. You open a wait against an address, and it resolves the moment a message for that address lands.

The wait is state on the platform, not a request that hangs on the edge. A suspended HTTP request dies long before a verification mail arrives; a wait survives because it is a row, and the agent polls it for an answer.

What to read next

The runtime tour covers the parts of the pipeline that produce these events 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.

Connect your domain and try the API.

Start free