GET /v1/sms/{id} and GET /v1/otp/{id} return status (the current state) and timeline (how it got there). When a user says “I never got the SMS”, the timeline is what you paste into the ticket.
Shape
at (RFC 3339) and status. The rest appear only when they apply.
delivered shows only when the carrier sends a receipt. Some routes never send one, so a message can stay at sent and still have reached the phone. The timeline never names an upstream; Robase is your processor. Robase does not publish a delivery-rate percentage, on this endpoint or anywhere else.
Reasons
reason is a stable machine token. Branch on it; do not parse it.
When the reason is not known,
reason is absent. New tokens may be added; existing ones are never renamed.
Refunds
A terminalfailed or blocked item carries refunded. It reflects the credit ledger, not a rule:
true: the charge is back on your balance.false: it is not. Today that is one case: an OTP burned by wrong verify attempts, which was delivered and is not refunded.
credit_cost on the parent object stays the message’s price, so a refunded message still shows what it would have cost.
Older messages
A hop is listed only when its timestamp is stored. Messages sent before this timeline existed may end short of theirstatus, for example a blocked message with only a queued hop. status is always authoritative.
Webhooks
sms.failed, sms.blocked, and otp.failed carry the same reason and refunded fields in data when they are known. Event names are unchanged. See SMS webhooks.