Twilio usually gives you the first 201 before it gives you the real problem.
That is what makes SMS bugs annoying.
You call the Messages API. Twilio accepts it. You get a SID back. Your app marks the notification as "sent" and everybody moves on.
Then the message never arrives.
Sometimes the destination number is bad. Sometimes the carrier rejects it. Sometimes the message moves through queued or sent and then lands in undelivered. The first API call still looked fine, so the debugging starts late.
That is the part I think a lot of Twilio examples skip. The useful test is not just "can I POST /Messages.json?" The useful test is "what does my app do after Twilio accepts the message but delivery goes sideways?"
The narrow workflow that matters
For local testing, the flow I care about is very small:
- send the message
- fetch the message again by SID
- check what status your app sees next
- confirm your retry, alerting, or customer-facing state does not pretend the message was delivered
In Twilio terms, that usually starts here:
curl -X POST \
"https://api.twilio.com/2010-04-01/Accounts/AC.../Messages.json" \
--data-urlencode "To=+14155551234" \
--data-urlencode "From=+15017122661" \
--data-urlencode "Body=Your order has shipped" \
-u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN"
and then immediately becomes:
curl \
"https://api.twilio.com/2010-04-01/Accounts/AC.../Messages/<message_sid>.json" \
-u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN"
That second read is where the truth starts showing up.
Where teams usually fool themselves
The easy mistake is treating 201 Created as success for the whole job.
It is only success for the first step: Twilio accepted your request and created a message resource. It is not proof that the carrier accepted it, not proof that the handset got it, and definitely not proof that your follow-up logic handled a bad delivery outcome correctly.
That gap creates a bunch of quiet bugs:
- your UI says "message sent" too early
- your retry job never runs because the app thinks the work is already done
- your webhook or polling code handles
deliveredbut notundelivered - support ends up debugging customer complaints that started as an async status transition, not a failed API request
What I would test before production
If I only had time for one narrow Twilio test, I would test the branch where the message gets accepted first and fails later.
That means checking:
- your app stores the SID from the first response
- you can fetch the message again and read the later status instead of assuming the first response told the whole story
- your callback or polling path updates the final state correctly
- the rest of your system reacts differently to
deliveredvsundelivered
This is the same reason SMS flows feel harder than they look in Postman. Postman proves the request shape. It does not prove the delivery story.
Why this is a better local test than another happy path
A happy path SMS demo is easy to trust too early.
You send one message, get one SID, see one success response, and tell yourself the integration is basically done.
But the production pain usually sits in the branch after that:
- carrier rejection
- invalid destination
- quiet delivery failure
- app state that never gets corrected after the async update
That is why I like narrow workflow tests more than broad "Twilio integration" tests. A small delivery-failure workflow tells you more than a generic sandbox smoke test ever will.
If you want to make this easier locally
I wrote a runnable Twilio docs portal on FetchSandbox because I kept wanting the same thing: send the message, fetch it again, and inspect the later state without jumping between too many tools.
The useful part is not just mocking the first response. It is being able to test the full "accepted first, failed later" branch before production traffic teaches it to you.
Curious how other people handle this one. Do you mostly poll message state, rely on callbacks, or just treat the first 201 as "good enough" until support says otherwise?
Top comments (0)