API reference
Messages
Read individual messages, and catch up on everything that changed while you were not listening.
This is your webhook recovery path
The list endpoint exists so that a webhook outage is recoverable. Poll from the moment your endpoint last answered and follow the cursor until there is nothing more — a delivery that exhausted its retries is gone from the webhook queue but still readable here.
List messages
/public/v1/messagesReturns messages whose status changed at or after a given moment, ordered so that a cursor can walk the whole set without gaps.
Query parameters
status_changed_sincestringRequiredWhere to resume from. Values older than seven days are silently clamped to seven days ago rather than refused.
ISO 8601 — clamped to the last 7 days
limitintegerOptionalHow many messages to return per page.
default 100, maximum 500
batch_idstringOptionalRestrict the results to a single batch.
26-character ULID
Request
Responses
Possible errors
| Code | Status | Meaning |
|---|---|---|
VALIDATION_FAILED | 422 | The request was malformed: a missing or invalid parameter, a bad idempotency key length, a rejected webhook URL, or an unsupported method or content type. |
UNAUTHORIZED | 401 | No key, a malformed key, a wrong secret, a revoked or expired key, or a suspended account. All indistinguishable by design. |
INSUFFICIENT_SCOPE | 403 | The key is valid but does not carry the scope this endpoint requires. The scope is named in the detail. |
RATE_LIMITED | 429 | The key's request limit was exceeded. Wait for the interval in Retry-After. |
INTERNAL_ERROR | 500 | An unexpected failure on SMSend's side. Quote the request id to support. |
Retrieve a message
/public/v1/messages/{message_id}Reads a single message by its identifier.
Path parameters
message_idstringRequiredThe message identifier, as returned by the list endpoint or a webhook.
26-character ULID
Request
Responses
Possible errors
| Code | Status | Meaning |
|---|---|---|
NOT_FOUND | 404 | No such resource — including one that belongs to another account, which is deliberately indistinguishable from one that never existed. |
UNAUTHORIZED | 401 | No key, a malformed key, a wrong secret, a revoked or expired key, or a suspended account. All indistinguishable by design. |
INSUFFICIENT_SCOPE | 403 | The key is valid but does not carry the scope this endpoint requires. The scope is named in the detail. |
RATE_LIMITED | 429 | The key's request limit was exceeded. Wait for the interval in Retry-After. |
INTERNAL_ERROR | 500 | An unexpected failure on SMSend's side. Quote the request id to support. |
Message statuses
A message ends in exactly one terminal state and never transitions again once it gets there.
| Value | Meaning |
|---|---|
SCHEDULED | Parked until its scheduled time. |
QUEUED | Waiting for the router. |
RETRY_SCHEDULED | A delivery attempt failed and another is scheduled. |
SUBMITTED | Handed to a provider, which has not yet returned a reference. A message stuck here is SMSend's problem, not yours. |
SENT | The provider accepted it and returned a reference. |
DELIVEREDTerminal | The handset confirmed receipt. |
FAILEDTerminal | Delivery failed. The reason is in failed_reason. |
EXPIREDTerminal | No delivery receipt arrived within 48 hours. |
REJECTEDTerminal | The provider or operator refused it. |
CANCELLEDTerminal | Dropped at send time, or cancelled with its batch. |
How the cursor works
Pages are ordered by the moment of the status change, with the message id as a tiebreaker. That makes the ordering total, so two messages that changed in the same millisecond cannot end up straddling a page boundary and one of them be missed.
The comparison is inclusive, so resuming from the returned cursor re-returns the last row of the previous page. Deduplicate on the message id together with its status.
When a page comes back empty the cursor is null. Keep your own position rather than advancing — moving forward past a quiet period would skip events that had not been written yet.