Resources
What a receipt tells you, when it is final, and how to reconcile it against an invoice.
Only terminal states close a message. An interim state is the operator saying it is still working, and treating one as final is the most common way a delivery rate ends up wrong — usually wrong in your favour, which hides the problem rather than showing it.
| State | Kind | Meaning |
|---|---|---|
| DELIVRD | Terminal | The handset acknowledged it. This is the only state that means delivered. |
| UNDELIV | Terminal | The operator gave up. Billed — it was accepted and attempted. |
| EXPIRED | Terminal | Validity period elapsed before the handset was reachable. |
| REJECTD | Terminal | The operator refused it after accepting the submission. |
| DELETED | Terminal | Cancelled at the operator. |
| ACCEPTD | Interim | Taken by the operator, not yet delivered. A further receipt follows. |
| ENROUTE | Interim | In the operator network. Not a final answer. |
SMPP does not say whether a message id is decimal or hexadecimal, so operators differ — and some hand back a decimal id at submit and echo the same value as hex on the receipt. The value is identical; the representation is not.
Unhandled, this fails in the most expensive way available: every receipt goes unmatched, nothing errors anywhere, and the platform reports zero delivered days later. We store both representations when the message is submitted, so whichever comes back matches. You do not have to do anything about it — it is documented because if you are migrating from a provider that got it wrong, this is what you were seeing.
submit → operator returns 255 (decimal) receipt ← operator sends id:ff (same value, hex)
Receipts are posted to your endpoint carrying the message id you were given at submit, so reconciliation is a join rather than a guess. Callbacks are signed, and a rotating secret keeps both the old and the new valid during the overlap — so rolling a secret does not drop reports while you deploy.
A receipt may arrive more than once. Operators retry, and a state can legitimately change. Treat your handler as idempotent on message id and state: applying the same terminal state twice should be a no-op rather than a second delivery.
Billing follows submission, not delivery — the operator charges for an accepted message whatever happens to it afterwards. So a report will always show fewer delivered than billed, and the gap is the operator’s failure rate rather than a discrepancy. What should never differ is the submitted count and the billed count; if those disagree, that is ours to explain and we would want to know.