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
StatusMeans
scheduledHeld for a future time.
queuedAccepted by us. Nothing has left the building.
routedA carrier has been chosen.
submittedThe carrier accepted it.
reconcilingWe lost the carrier's response and are asking them what happened. Not a failure.
sentOn the network.
deliveredThe handset confirmed it. Terminal.
failedIt will not arrive. Terminal.
expiredThe carrier gave up retrying. Terminal.
rejectedWe refused it before spending anything. Terminal, and not billed.
cancelledYou 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.

CodeMeansWorth retrying?
INVALID_DESTINATIONNot a real number, or not one that can receive SMS.No
UNROUTABLE_COUNTRYThat country is not enabled on your account, or the range is blocked.No — ask us to open it
SENDER_NOT_PERMITTEDThat sender is not allowed for that destination.No — change the sender
SENDER_NOT_REGISTEREDThe destination requires a registered sender and yours is not yet approved.No — finish registration
CARRIER_REJECTEDThe carrier refused it, with no more specific reason.Sometimes
SPAM_FILTEREDFiltered as spam. Usually the content, occasionally the sender's reputation.No — change the message
HANDSET_UNREACHABLEOff, out of coverage, or out of storage.Yes, later
OPTED_OUTThis person has opted out of your messages, or marketing was sent without recorded consent.No. Never.
QUIET_HOURSMarketing, outside the hours it may be sent.Yes — it will go in the morning
INSUFFICIENT_FUNDSOut of credit, or the hourly spend ceiling was reached.Yes, after topping up
RATE_LIMITEDToo fast for the carrier.Yes, with backoff
PROVIDER_ERRORThe carrier had a problem. We may already have tried another.Yes
EXPIREDThe carrier's validity period ran out.Yes, if it is still worth sending
UNKNOWNA 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.