Get an API key

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:

StatusMeaning
queuedAccepted and waiting for a delivery attempt. Every message starts here.
sendingA worker has claimed it and is talking to the recipient’s server.
deliveredThe 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.
bouncedPermanently refused (a 5xx). The address is suppressed immediately.
complainedThe recipient reported it as spam. The address is suppressed and cannot be un-suppressed.
failedWe could not deliver it after exhausting retries — usually repeated temporary failures or a server we could never reach.
rejectedWe 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

GET/v1/messages/:idmessages:read
const 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
FieldNotes
statusOne of the seven above.
attemptsHow 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.
contentWhat 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.
recordIntactWhether 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.

StatusWhen
200The message and its waybill.
404 not_foundNo such message, another account’s message, or an id that is not a message id.

List messages

GET/v1/messagesmessages:read

Newest first, keyset-paginated. See pagination — and in particular, loop on hasMore.

ParameterTypeNotes
limitinteger optional1–100, default 25.
beforestring optionalA message id — the nextCursor from the previous page. An id of the wrong kind returns 400 invalid_request.
statusstring optionalOne of the seven statuses. Anything else is a 400.
tostring optionalExact recipient address. This is the one an integration wants — “show me this customer’s mail” — and it resolves through an index.
searchstring optional1–200 characters, matched case-insensitively against both recipient and subject. For a half-remembered address or a word from a subject line.
domainstring optionalA dom_ id, to restrict to one sending domain.
from / untilstring optionalISO 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

GET/v1/stats/messagesmessages:read

Sending 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.

ParameterTypeNotes
daysinteger optional1–365, default 30.
domainstring optionalA dom_ id.
tzstring optionalAn 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 days

The 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.

NextSuppressionsWhy an address stops receiving mail, and which entries can never be removed.