Receiving email

Point an MX record at us and the email addressed to your domain arrives as JSON on your webhook, with SPF, DKIM and DMARC already evaluated.

Routes

A route is a verified domain plus a local part. POST /email/inbound/routes creates one; the domain has to be verified for sending first, because that DNS proof is what shows the domain is yours. Accepting mail for a domain nobody proved would mean receiving somebody else’s correspondence, and the real owner would never know.

FieldNotes
domain_idA domain of yours that already passes verification. Anything else is a 400 telling you to publish the DNS records first.
local_partThe part before the @, or * for every address on the domain. A specific route wins over the wildcard, so support@ and a catch-all can coexist.
webhook_endpoint_idWhere email.received for this route goes. Omit it and every endpoint of the project that subscribes to the event receives it.
retain_daysHow long the raw copy is kept, 1 to 30. The bucket expires at 30 either way, so a longer promise would be a promise we cannot keep.
enabledTurn receiving off without deleting the route, and without losing what already arrived.

Then publish the MX record for the domain or subdomain, pointing at the host support gives you. Until we reload the mail server with your domain on it, senders get a bounce: tell us when the route is live.

The plus tag is stripped when routing: a reply that arrives at support+ticket-42@ matches the route support. That is why a local_part containing + is refused: it could never match anything.

The email.received event

Every message that matches a route becomes one email.received webhook, signed and retried like the rest. The payload carries the envelope, the parsed headers, text and html, the attachment catalogue and the authentication verdict. Subscribe to it on the webhooks page of the panel.

FieldWhat it holds
inbound_message_idUse it on GET /email/inbound/{id}, on /raw, and as in_reply_to_inbound when you answer.
envelope_from / envelope_toWhat the sending server declared. Not the same as the From: and To: headers, and the difference is the point.
from / to / cc / subjectThe parsed headers, as objects with email and optional name.
text / htmlThe body, truncated at 256 KB with a truncated flag. The full message is always at /raw.
attachmentsA catalogue: index, filename, content_type, size_bytes, content_id, inline. The bytes stay in the raw message and come from the attachment endpoint.
authenticationspf, dkim and dmarc, plus details. Read the next section before trusting a From: address.

Anyone can write any address in a From: header, so the header alone proves nothing. dkim is verified against the sender’s own DNS key; spf comes from what our mail server stamped on arrival; dmarc is the alignment of the From: domain with whichever of the two passed. A dmarc of pass is the one that means the sender is who the message says.

none is not fail. none means nothing could be evaluated, which is common for senders that do not sign at all; fail means a mechanism ran and did not match. Treating the two as the same would mark half of legitimate mail as forged.

Reading what arrived

GET /email/inbound lists messages in arrival order, oldest first, paged by cursor. That direction is deliberate: polling means walking forward in time, so you store the cursor, sleep, and come back asking what happened after it. Filter with route_id and since.

EndpointWhat it returns
GET /email/inboundThe page of messages, ascending by received_at, with next_cursor.
GET /email/inbound/{id}One message, with the same fields the webhook carried.
GET /email/inbound/{id}/rawThe original .eml, byte for byte: full headers, intact DKIM signature, embedded attachments.
GET /email/inbound/{id}/attachments/{index}One attachment, extracted from the raw message on the spot.
DELETE /email/inbound/{id}Deletes the stored copy and the record. This is how you honour an erasure request.

To answer a message and have the reply land in the same conversation on the sender’s side, send with in_reply_to_inbound set to the inbound_message_id. We fill In-Reply-To and References from the received message; headers you set yourself always win.

How long it is kept

The raw copy lives for the route’s retain_days and is then deleted, file and record together. The parsed fields you see on the message are not affected; only /raw stops working, and it answers 410 rather than pretending the message is gone.

Inbound is free: receiving costs nothing, on any plan. What it is not is a mailbox. Nothing here stores mail for a person to read later, and there is no IMAP: the message becomes an event, you act on it, and the copy expires.