Sending
Messages and the waybill
A waybill is the complete record of one message: every event it passed through, what was in it, and the verbatim SMTP conversation with the receiving server. It is append-only and hash-linked, so you can verify independently that nothing was rewritten afterwards.
Message statuses
A message has exactly one status at a time, and it is the last thing that happened to it. These seven are the complete set:
| Status | Meaning |
|---|---|
| queued | Accepted and waiting for a delivery attempt. Every message starts here. |
| sending | A worker has claimed it and is talking to the recipient’s server. |
| delivered | The receiving server accepted it. This is as far as we can see — what the recipient does with it afterwards is not visible to any sender. |
| bounced | Permanently refused (a 5xx). The address is suppressed immediately. |
| complained | The recipient reported it as spam. The address is suppressed and cannot be un-suppressed. |
| failed | We could not deliver it after exhausting retries — usually repeated temporary failures or a server we could never reach. |
| rejected | We refused it ourselves rather than attempting it. |
bounced and failed are not the same thing. A bounce is the recipient’s server saying “never send here again”, and we suppress. A failure is us giving up after retries, and we do not suppress — a mailbox that was full for an afternoon is not a dead address.
Read one message
/v1/messages/:idmessages:readconst message = await posthaste.messages.get('msg_AZLm3kQ8T2Sf9pXbNc7HrQ')
message.status // 'bounced'
message.recordIntact // true — the hash chain was re-verified on read
message.smtp // { code: 550, message: '5.1.1 No such user here' }
message.content?.text // null once the body ages out of your retention window
message.waybill // every event, in order, with its hash and prevHash| Field | Notes |
|---|---|
| status | One of the seven above. |
| attempts | How many delivery attempts have been made. |
| smtp | { code, message } from the last attempt — the receiving server’s own words, including its diagnostic text. null before the first attempt. |
| content | What was sent: text, html, raw (the composed, signed message as it went on the wire), replyTo and your custom headers. Bodies are encrypted at rest and decrypted for this response only. null once the body has aged out of your plan’s retention window — which is different from an empty body, and the message record itself lives on much longer. |
| waybill[] | Every event, oldest first: seq, type, at, detail, hash, prevHash. The types are the same ones webhooks carry — see the list. |
| recordIntact | Whether this message’s events still hash correctly. Verified on every read, not claimed. When false, recordProblem says what broke. |
The hash chain
Each event carries hash and prevHash. The chain is per account, not per message, so seq values within one message are not consecutive — other messages’ events are interleaved between them, and that is correct rather than a gap.
Which is why recordIntact here proves these events’ contents are unaltered, and cannot prove nothing was deleted. For that, replay the whole chain with GET /v1/account/verify.
| Status | When |
|---|---|
| 200 | The message and its waybill. |
| 404 not_found | No such message, another account’s message, or an id that is not a message id. |
List messages
/v1/messagesmessages:readNewest first, keyset-paginated. See pagination — and in particular, loop on hasMore.
| Parameter | Type | Notes |
|---|---|---|
| limit | integer optional | 1–100, default 25. |
| before | string optional | A message id — the nextCursor from the previous page. An id of the wrong kind returns 400 invalid_request. |
| status | string optional | One of the seven statuses. Anything else is a 400. |
| to | string optional | Exact recipient address. This is the one an integration wants — “show me this customer’s mail” — and it resolves through an index. |
| search | string optional | 1–200 characters, matched case-insensitively against both recipient and subject. For a half-remembered address or a word from a subject line. |
| domain | string optional | A dom_ id, to restrict to one sending domain. |
| from / until | string optional | ISO 8601 datetimes bounding createdAt. from is inclusive, until exclusive. |
// One page.
const page = await posthaste.messages.list({
status: 'bounced',
from: '2026-08-01T00:00:00Z',
limit: 50,
})
page.data // MessageSummary[]
// Or every page, looping on hasMore rather than on nextCursor.
for await (const message of posthaste.messages.autoPaginate({ status: 'bounced' })) {
await suppress(message.to)
}Rows carry smtpCode rather than the full SMTP exchange — fetch the message to read what the server actually said.
Daily statistics
/v1/stats/messagesmessages:readSending volume by day, with the same window immediately before it for comparison. Every day in the range comes back including the empty ones — a chart drawn only from days with traffic closes its own gaps, so a week of outage renders as an unbroken line.
| Parameter | Type | Notes |
|---|---|---|
| days | integer optional | 1–365, default 30. |
| domain | string optional | A dom_ id. |
| tz | string optional | An IANA zone such as Europe/London, default UTC. Buckets land on your midnight rather than UTC’s. An unknown zone returns 400 invalid_request. |
const stats = await posthaste.messages.stats({ days: 30, tz: 'Europe/London' })
stats.totals.deliveryRate // 99.4 — a PERCENTAGE, over settled mail
stats.totals.settled // total minus pending: the denominator
stats.previous // the same window immediately before, to compare
stats.thresholds // { complaintRate: 0.3, bounceRate: 5 }
stats.data // one row per day, including the zero daysThe rates are over settled mail, not everything sent. Counting messages still in flight as “not delivered” makes the rate dip during any burst and recover on its own, which reads as a fault when nothing is wrong. settled is total minus pending, and it is the denominator.
thresholds ships with the data so a chart can draw the lines that matter: 0.3% complaints and 5% bounces are the levels at which the large mailbox providers start acting, not numbers we invented.
NextSuppressions →Why an address stops receiving mail, and which entries can never be removed.