WARP guide
Message Delivery and Troubleshooting
What happens to a message after you send it, what every message state means, how delivery receipts and inbound webhooks behave, and how to work out why a message did not arrive.
This guide covers what happens after a message leaves your application: the states it moves through, what the message log shows, how delivery receipts work, how inbound messages reach you, and how to diagnose one that never arrived.
It assumes your account is already able to send. Getting to that point — number ownership, SMS enablement, campaign assignment and API scopes — is covered in SMS and MMS API Readiness, and the registration process behind it in Messaging and 10DLC Registration. If a message is being rejected at send time with a permission or campaign error, start with those guides; this one picks up once WARP has accepted the message.
Three Different Acceptances
Most delivery confusion comes from treating one acceptance as all three. They are separate events, and each can succeed while the next fails.
- WARP accepted the message.
POST /v1/messagesreturned 201 and the message was stored. This means your request was well-formed, thefromnumber belongs to you, and it is enrolled in an approved campaign. It says nothing about the carrier. (A 201 whosestatusalready readsfailedmeans the hand-off failed immediately — the message is stored, but it never reached a carrier.) - The carrier accepted the submission. The upstream messaging partner acknowledged the message and issued its own identifier. The message is now their responsibility to deliver.
- The handset received it. A delivery receipt came back confirming the message reached the device.
A message can sit correctly at step 1 or 2 for a long time and still never reach step 3. Only step 3 is delivery.
Message States
Every message carries a status. These are the only values a stored message ever has:
| State | Direction | Meaning | Terminal |
|---|---|---|---|
queued | Outbound | Stored by WARP and queued for submission to the carrier. No carrier acknowledgement yet. | No |
sent | Outbound | The carrier accepted the submission and returned its own message identifier. | No |
delivered | Outbound | A delivery receipt confirmed the message reached the handset. | Yes |
undeliverable | Outbound | A delivery receipt reported the carrier could not deliver it — expired, undeliverable, rejected or deleted. | Yes |
failed | Outbound | The message never got as far as a carrier acknowledgement: WARP could not hand it off, or the carrier rejected the submission outright. error_code carries the reason. | Yes |
received | Inbound | A message arrived on one of your numbers. Inbound messages are recorded in one state and do not advance. | Yes |
Two properties are worth relying on:
- State never moves backwards. A late or out-of-order receipt cannot demote a
deliveredmessage back tosent. - Intermediate carrier receipts are not surfaced. Carriers may emit interim states while a message is in flight. WARP records only the ones that map to the states above, so a message in flight stays on
sentrather than flickering through vendor-specific values.
failed and undeliverable are both terminal failures but mean different things. failed is a problem on the way to the carrier — retrying may work. undeliverable is the carrier's verdict about the destination — retrying the same message to the same handset usually will not.
The Message Log
Messaging → Messages (/messaging/messages) lists inbound and outbound messages for your account, most recent first. It requires the messaging:messages:read scope.
Columns:
- Direction —
OutorIn. - From / To — the sending and destination numbers.
- Body — the message text, truncated in the table; the full text is in the detail view.
- Segments — how many parts the message occupies on the wire. See below.
- Status — the state from the table above.
- Created — when WARP recorded the message.
- Actions — inbound rows offer Reply, which sends from your number back to the original sender.
Selecting a row opens the detail view, which adds the message ID, encoding, error_code when a failure has one, the carrier's own message identifier once it has been issued, and the last-updated timestamp. The carrier identifier is what support and your carrier will ask for when tracing one specific message.
Two filters sit above the table, one for direction and one for status. The screen loads the 50 most recent matching messages and has no pager — for anything beyond that, page through GET /v1/messages.
Reading Status From the API
GET /v1/messages/{id} returns one message with its current state. This is how an integration checks outcome:
$ curl https://api.warp.ringer.tel/v1/messages/<message-id> \
-H "Authorization: Bearer $WARP_API_KEY"
GET /v1/messages lists messages, most recent first, and accepts direction (INBOUND or OUTBOUND), status, page and per_page (maximum 100). Page until a page comes back with fewer rows than you asked for.
GET /v1/messages/stats returns aggregate counts for your account — total, inbound, outbound, delivered, failed — plus a per-campaign outbound and delivered breakdown. The failed count there covers both terminal failure states, failed and undeliverable.
All three need messaging:messages:read.
POST /v1/messages accepts an optional status_callback URL. It is recorded on the message, but WARP does not currently post outbound status updates to it — poll GET /v1/messages/{id} instead, and do not build an integration that waits on an outbound status callback.
Sending Safely on Retry
Supply an Idempotency-Key header on POST /v1/messages. A first successful create returns 201; a repeat with the same key returns 200 and the original message rather than sending a second copy. Without a key, a client-side retry after a timeout sends the message twice.
Why a Final State Can Lag
delivered and undeliverable are set from a delivery receipt, which arrives asynchronously from the carrier — seconds later for a handset that is on and in coverage, much later for one that is off, out of coverage or roaming. Until the receipt arrives, a message correctly reads sent.
Some destinations never return a receipt at all. A message that stays on sent indefinitely has been accepted by the carrier and simply has no confirmation of its fate: it is not evidence of failure, and it is not evidence of delivery either. Treat a long-lived sent as unknown, not as a fault, and check whether the recipient actually received it before escalating.
Segments
Message length is measured in segments, not characters, and segments are what carriers count.
Encoding is chosen automatically from your text:
- GSM-7 — the standard SMS alphabet. 160 characters in a single segment; 153 per part once a message is split, because each part carries a small header.
- UCS-2 — used as soon as your text contains a single character outside the GSM-7 set. 70 characters in a single segment; 67 per part when split.
The consequence catches people out: one emoji, one curly apostrophe, or one accented character pasted from a word processor moves the entire message to UCS-2 and drops its capacity from 160 characters to 70. A 90-character message is one segment as plain text and two segments with one emoji in it.
The log's Segments column and the segments field show the real count for each message. If a template is unexpectedly multi-segment, look for smart quotes, em dashes and non-breaking spaces before you look at length.
Inbound Messages and Webhooks
Messages sent to your numbers are recorded as inbound in the message log whether or not you have configured a webhook. A message addressed to a number that is not assigned to your account produces no record — nothing is written and nothing is delivered.
To have inbound messages pushed to your application, configure a webhook per number. There is no account-level or campaign-level webhook. On /numbers, open the number and use its SMS tab:
- Webhook URL — where inbound messages are posted.
- Signing secret — used to sign each delivery, so you can verify it came from WARP.
- Fallback URL — used after repeated failures against the primary URL.
- MMS enabled — whether MMS is accepted on the number.
The same fields are settable with PATCH /v1/numbers/{tn}/sms (inbound_webhook_url, inbound_webhook_secret, fallback_inbound_url, mms_enabled), which needs numbers:write. DELETE /v1/numbers/{tn}/sms clears the SMS configuration for a number.
The Delivery WARP Sends
A POST with Content-Type: application/json and a body of:
{
"id": "…",
"direction": "inbound",
"type": "SMS",
"from": "13125551212",
"to": "13039813633",
"body": "…",
"num_segments": 1,
"received_at": "2026-09-16T14:02:11Z"
}
When a signing secret is set, the request carries an X-Warp-Signature header of the form t=<unix-seconds>,v1=<hex>, where v1 is an HMAC-SHA256 over the string <t>.<raw request body> keyed with your signing secret. Verify it against the raw body bytes, before any JSON parsing or re-serialization — re-encoding the body changes the bytes and the signature will not match.
Webhook deliveries originate from 35.254.220.213. If your endpoint is IP-restricted, allowlist that address.
Retry Behaviour
Your endpoint must answer with a 2xx status within 10 seconds. Anything else — a non-2xx response, a connection error, or a timeout — counts as a failed attempt.
Failed attempts are retried on a widening schedule: 30 seconds, then 1 minute, 5 minutes, 15 minutes, 1 hour, and every 2 hours after that.
After three consecutive failures against the primary URL, WARP switches to the fallback URL if you have configured one, and continues retrying there.
A delivery is abandoned once it is more than 24 hours old. At that point it is dead-lettered and no further attempt is made — the inbound message is still in your message log, but it will never be pushed to you. An endpoint that is down for a day loses those pushes permanently, so treat GET /v1/messages?direction=INBOUND as your reconciliation path after an outage.
Because retries are real, your handler must be idempotent: the same message ID can arrive more than once. Answer 2xx as soon as you have durably stored the message — returning a non-2xx after you have already processed it guarantees a duplicate.
Working Out Why a Message Did Not Arrive
Work down this list in order. Each step is cheap and rules out a whole class of cause.
- Find the message. Fetch it by ID, or narrow the message log with the direction and status filters and look for the recipient. If there is no record at all, WARP never accepted it — check your request and its response, not delivery.
- Read the state.
queuedandsentare in flight.deliveredmeans it reached the handset and the recipient's device or app is the next place to look.failedandundeliverableare covered below. - On
failed, readerror_code. A submission rejected by the carrier carries the carrier's own code. A hand-off failure inside WARP is recorded assubmit_failed— the message is stored but never reached a carrier, and it is safe to resend. - On
undeliverable, the carrier reached a verdict. Common causes: the number is not in service, is a landline that cannot receive SMS, is a device that has been off long enough for the message to expire, or the carrier rejected it. Confirm the destination number is a real mobile number before retrying. - On a long-lived
sent, ask the recipient. No receipt has arrived. Carrier-level filtering of A2P traffic sometimes shows up exactly this way — accepted, never confirmed, never delivered. - Check the message content against your registered campaign. Traffic that does not match the use case and sample messages you registered is a common cause of silent carrier filtering, and it looks exactly like this: accepted, never confirmed, never delivered. See Messaging and 10DLC Registration.
- Check consent. A recipient who has replied STOP will not receive further messages, and continuing to send to them puts the rest of your traffic at risk.
- For missing inbound messages, check the log first. If the message is in the log but never reached your endpoint, the problem is your webhook — check reachability, the 10-second response deadline, and whether deliveries were dead-lettered during an outage. If the message is not in the log at all, confirm the number is assigned to your account and SMS-enabled.
Before contacting support, collect: the WARP message ID, the carrier's message identifier from the detail view, the sending and destination numbers, the timestamp, the current state and any error_code, and the campaign the sending number is assigned to.
Scopes and API Reference
Reading message state and history uses messaging:messages:read:
GET /v1/messagesGET /v1/messages/:idGET /v1/messages/stats
Sending uses messaging:messages:write:
POST /v1/messages
Per-number SMS and webhook configuration uses numbers:read to inspect and numbers:write to change:
PATCH /v1/numbers/:tn/smsDELETE /v1/numbers/:tn/sms