Reference
Live tail
The message log answers what happened. This answers what is happening — the same append-only events, delivered as they are written, usually within a tenth of a second of the send being accepted.
The endpoint
/v1/messages/tailmessages:readA text/event-stream response that stays open. It carries the same events the audit log holds and the waybill shows, so nothing here is a second source of truth — it is the one record, sooner.
Nothing polls, on either side. The connection is idle until an event lands, at which point the database wakes it. You are not being charged a request per second for a tab left open, and neither are we.
Filters
The same three the message log takes, meaning the same three things. Everything else is deliberately absent: a tail is for watching a slice of live traffic, and a search box on a stream that has not happened yet would be a search over nothing.
| Parameter | Effect |
|---|---|
tag | Repeatable, and ANDed — ?tag=a&tag=b means both, exactly as on the message log. Up to ten. |
stream | One stream, by slug. Mail on any other stream is not on this tail at all. |
status | The message’s status as of the moment the event is read, not at the moment it happened — a message that later bounces has all of its events read as bounced. Worth knowing, because a status is a property of a message and not of an event. |
replay | How much recent history to open with, up to 100. Defaults to 20 so the screen is not blank. Ignored on a resume — see below. |
after | Resume from this seq, for callers that cannot set a header. Last-Event-ID wins when both are present. |
What arrives
| Event | Meaning |
|---|---|
ready | The stream is open. Carries the filters as we understood them, the heartbeat interval and the idle timeout. Show a connected state on this. |
message | One event on one message. Carries seq, the event type, when it happened, the message with its current status, stream and tags, and the event payload as detail. The SSE id: is the seq. |
heartbeat | Every 15 seconds, whether or not anything is happening. Its absence is the signal — see the note below. |
bye | We closed the stream, and the reason says why: idle, error or unavailable. Reconnect and resume. |
Only message carries an id:, so a heartbeat never moves your resume position.
Watch for the heartbeat stopping, not just for an error. A TCP connection can be dead for minutes without either end being told, and a tail that has silently died looks exactly like a tail with nothing to report. If two heartbeats do not arrive, treat the stream as gone and reconnect — the resume below means it costs you nothing to be wrong about that.
A dropped connection loses nothing
Every message event carries your account’s seq as its SSE id. Reconnect with that value in Last-Event-ID — which a browser does for you — and everything after it is replayed, in order, before the stream goes on following.
Move your cursor after you have handled an event, never before. That is the one rule, and it is what turns a reconnect from a hope into a guarantee: at worst you see an event twice, which you can spot by seq, instead of never seeing it at all.
# Every message event carries its account seq as the SSE id.
# Remember the last one you handled, and hand it back.
curl -N \
-H "Authorization: Bearer $POSTHASTE_API_KEY" \
-H "Last-Event-ID: 4187" \
"https://api.posthastemail.dev/v1/messages/tail"
# Or, for anything that finds setting a header awkward:
curl -N \
-H "Authorization: Bearer $POSTHASTE_API_KEY" \
"https://api.posthastemail.dev/v1/messages/tail?after=4187"
# Both mean the same thing: everything after 4187, in order, then follow.Limits
| Limit | Value | What happens |
|---|---|---|
| Concurrent tails | 5 per account | A sixth is refused with 429 too_many_streams, which names the limit. Waiting does not fix it — closing a tail does, immediately. |
| Idle timeout | 15 minutes | With no message event in that window the stream closes with bye {"reason":"idle"}. Reconnect and resume; heartbeats do not reset it, deliberately. |
| Reconnect delay | 3 seconds | Sent as retry: at the top of the stream. Honour it — every reconnect costs an authentication and a replay. |
Account-level events with no message — anything the audit log holds that is not about a specific send — are not on this tail. It streams mail moving, so an event with no recipient, status, stream or tags would be a blank line no filter could ever match. The audit log has all of them.
Examples
curl -N \
-H "Authorization: Bearer $POSTHASTE_API_KEY" \
"https://api.posthastemail.dev/v1/messages/tail?replay=5"
# -N matters. Without it curl buffers the response and you see nothing
# until the connection ends, which for a stream is never.
retry: 3000
event: ready
data: {"resumedFrom":null,"filters":{"status":null,"stream":null,"tags":[]},"heartbeatSeconds":15,"idleTimeoutSeconds":900}
id: 4187
event: message
data: {"seq":4187,"id":"evt_AZLm3kQ8T2Sf9pXbNc7HrQ","type":"accepted","at":"2026-08-26T09:14:02.418Z","message":{"id":"msg_9Kd2XbQ1T4Rf8pLmNc3HrA","to":"[email protected]","subject":"Your password reset","status":"queued","stream":"transactional","tags":["password-reset"]},"detail":{}}
event: heartbeat
data: {"at":"2026-08-26T09:14:17.002Z"}NextDelivery analytics →Delivery rate and time to inbox, p50 and p95, for every receiving provider and for each of your own tags — with nothing invented where there is nothing to measure.