Delivery reports
Accepted is not sent, sent is not delivered, and a carrier that has gone quiet is not the same as one that said no. Each of those is a different status here.
The lifecycle
scheduled ──→ queued ──┬──→ rejected (terminal, unbilled)
│ └──→ routed ──→ submitted ──┬──→ sent ──┬──→ delivered
│ │ └──→ failed
└──→ cancelled (terminal) ├──→ failed ──→ [failover] ──→ routed
└──→ unknown ──→ reconciling| Status | Means |
|---|---|
scheduled | Held for a future time. |
queued | Accepted by us. Nothing has left the building. |
routed | A carrier has been chosen. |
submitted | The carrier accepted it. |
reconciling | We lost the carrier's response and are asking them what happened. Not a failure. |
sent | On the network. |
delivered | The handset confirmed it. Terminal. |
failed | It will not arrive. Terminal. |
expired | The carrier gave up retrying. Terminal. |
rejected | We refused it before spending anything. Terminal, and not billed. |
cancelled | You cancelled a scheduled message. Terminal. |
Status only moves forwards
Carriers emit receipts out of order as a matter of routine — delivered lands before sent more often than you would believe, because they come from different systems inside the carrier.
Every status has a rank, and an update applies only if its rank is at least the current one. All terminal statuses share the top rank, so once a message has finished, nothing moves it in either direction.
The consequence for you: your webhook will never fire backwards, and a message that reads delivered stays delivered. Without that rule a latesent would overwrite delivered, the message would appear to be in flight forever, and the usage event would be written twice.
Failure codes
Normalised across every carrier we use. The carrier's own code is kept for our conversations with them; you get a vocabulary that means the same thing whoever carried the message.
| Code | Means | Worth retrying? |
|---|---|---|
INVALID_DESTINATION | Not a real number, or not one that can receive SMS. | No |
UNROUTABLE_COUNTRY | That country is not enabled on your account, or the range is blocked. | No — ask us to open it |
SENDER_NOT_PERMITTED | That sender is not allowed for that destination. | No — change the sender |
SENDER_NOT_REGISTERED | The destination requires a registered sender and yours is not yet approved. | No — finish registration |
CARRIER_REJECTED | The carrier refused it, with no more specific reason. | Sometimes |
SPAM_FILTERED | Filtered as spam. Usually the content, occasionally the sender's reputation. | No — change the message |
HANDSET_UNREACHABLE | Off, out of coverage, or out of storage. | Yes, later |
OPTED_OUT | This person has opted out of your messages, or marketing was sent without recorded consent. | No. Never. |
QUIET_HOURS | Marketing, outside the hours it may be sent. | Yes — it will go in the morning |
INSUFFICIENT_FUNDS | Out of credit, or the hourly spend ceiling was reached. | Yes, after topping up |
RATE_LIMITED | Too fast for the carrier. | Yes, with backoff |
PROVIDER_ERROR | The carrier had a problem. We may already have tried another. | Yes |
EXPIRED | The carrier's validity period ran out. | Yes, if it is still worth sending |
UNKNOWN | A carrier code we do not map. | Judge by the message |
UNKNOWN is deliberate rather than lazy. Guessing at an unfamiliar carrier code would hand a message retry semantics nobody reasoned about; the raw code is kept alongside so we can ask the carrier and then map it properly.
Reconciliation
When we submit to a carrier and the connection times out, the message becomesreconciling, not failed. The carrier may well have accepted and sent it; we merely lost the reply. Reporting it as failed would let the router try a second carrier and deliver the same message twice.
A sweep then asks the carrier what actually happened and finishes the message honestly — which sometimes means failing it, minutes later, with the real reason.