Messages
What mailfixture stores when an email arrives, how your test gets it the moment it lands, and when it disappears again.
From SMTP to queryable
Delivery is synchronous — there is no queue between our SMTP receiver and the API. When a sender finishes transmitting, we parse the MIME, run extraction, store the message, and wake every long-poller on that inbox in the same operation. By the time the sending server sees its 250 OK, the message is already answerable through the API. That's what makes sub-second OTP tests possible.
Each message also records whether the delivering SMTP session negotiated STARTTLS, exposed as the boolean tls field — so a suite can assert that its email provider actually encrypts delivery.
The message object
Lists return summaries: id, received_at, from_addr, to_addrs, subject, tls. GET /v1/messages/{id} returns the full detail:
{
"id": "0198a3f7-…",
"inbox_id": "0198a3e2-…",
"received_at": "2026-07-07T14:18:02Z",
"from_addr": "noreply@acme.test",
"to_addrs": ["signup-run-41@mxsink.sh"],
"subject": "Your verification code",
"tls": true,
"text_body": "Your code is 482913…",
"html_body": "<html>…",
"headers": { …every header, as received… },
"extracted": { …otp, links, text — see Extraction… },
"attachments": [ …metadata — see Attachments… ],
"raw_size": 18244,
"expires_at": "2026-07-10T14:18:02Z"
}
Long-polling with wait
GET /v1/inboxes/{id}/messages?wait=45 holds the request open until a matching message arrives, then returns immediately — no sleep-and-retry loop on your side. A single request waits up to 60 seconds (larger values are clamped, not rejected); the SDK helpers chain requests under one client-side deadline, so wait_for_otp(timeout=120) just works.
Two more query parameters shape the result:
since — an RFC 3339 timestamp; only messages received after it are returned. Use the received_at of the last message you saw as a cursor. The SDKs manage this internally.
match — a deliberately simple filter: subject:, from:, or to: prefix followed by a case-insensitive substring; a bare term matches the subject. match=from:acme.test ignores the password-reset email that races your OTP email into the same inbox.
wait is the intended consumption model for tests — it's cheaper and simpler than standing up an endpoint. Reach for webhooks when you're wiring pipelines, not assertions.
Retention
Every message carries expires_at = received_at + your plan's retention window: 3 days on free, 14 on solo, 30 on team and scale. An hourly sweep deletes expired messages — along with their extraction results, attachments, and raw MIME. Test email is ephemeral by design; if you need something longer-lived, export it while it exists.
Messages also disappear earlier when you delete their inbox, clear it, or the inbox's TTL expires.
Size and raw storage
Messages are capped at 10 MB — larger transfers are rejected during the SMTP session. The raw MIME of every message is kept for its lifetime (it backs the Raw tab, attachment downloads, and embedded-image rendering); raw_size reports its size in bytes. Where the raw bytes physically live is invisible to the API.
Receiving caps
Two caps bound how much mail an account accepts, both enforced during the SMTP session:
Daily cap — 100 (free), 2,000 (solo), 10,000 (team), 50,000 (scale) messages per day. Beyond it, senders get a transient 452: well-behaved mail servers queue and retry, so a burst spread over the day still lands.
Monthly quota — 100 on free as a hard stop (550) with no overage. Paid plans have no monthly stop: messages beyond the included quota bill as overage at $3.50 per 1,000 instead of bouncing. See pricing.