Why a Twilio retry sends a second SMS and a second bill
Twilio's Messages create has no idempotency key. A second POST is a second SMS with its own SID, and num_segments can bill that same body more than once.
Key takeaways
- -In this article we explore the Twilio API for agent SMS, and why a retry can bill you twice.
- -POST to Messages.json creates a new Message. That create schema has no idempotency key.
- -The response SID is the receipt. A timeout is not proof the first text failed to send.
- -num_segments is how many segments that one body was split into. A second create bills the message again.
The model sends a one-time code. The HTTP call times out before the JSON comes back. The agent posts the same body again. The customer receives two texts, ten seconds apart, with two codes. They type the first. Your Twilio account has two Message resources, and each one is a charge. The loop did not feel expensive from the prompt. The bill is per create.
This post is how Twilio bills that loop: the create call, num_segments, the Message SID you must store, and the retry that should not be a second POST.
POST Messages.json creates a billable message
Twilio's Message resource creates an outbound message with POST https://api.twilio.com/2010-04-01/Accounts/{AccountSid}/Messages.json. The required field in the create schema is To, in E.164 form, or a channel address such as whatsapp:+15552229999. You also send a From number on the account, or a messaging service, and a Body.
The response is a Message. It has a SID that starts with SM for SMS, a status, and num_segments. Twilio's own example of this POST says the request causes Twilio to send the text. There is no idempotency field on that create. A second POST with the same To and Body is a second message. Twilio does not look at the previous body and collapse them.
That is the whole cost bug. Card APIs often store a key and return the first result. This SMS create stores a new message. Log every call so the SID is on disk before anyone discusses a retry. If you cannot find a SID, you still do not know the first POST failed. A timeout is not proof Twilio never queued the text.
The Twilio catalog page is the method list. swy search twilio then swy list methods. Enable one send method. Do not enable a second "retry send" that is the same POST with a new body.
Country prices are on Twilio's pricing pages and they move. This post will not invent a per-message dollar amount. Whatever the current price is for that destination, a second accepted create spends it again.
num_segments multiplies a single body
num_segments tells you how Twilio split the body. A short code that fits in one segment is one segment. A long alert, or one with characters that force Unicode, can be two or more. You pay for the segments of that message, then you pay again if you create another message.
An agent that "adds a little context" to an OTP can push a one-segment text into two segments and double the unit cost of every send, before any retry. Keep the body short enough to stay one segment, and record num_segments from the response so a prompt edit that lengthens the text shows up the same day.
StatusCallback is a URL you pass on create. Twilio POSTs there when status changes. For many SMS and MMS messages, a delivered or undelivered callback is how you learn the carrier result. The properties vary by channel. The point for the agent is smaller: queued or sent already means the message exists. Wait for the callback. Do not create a replacement because the callback has not arrived yet.
What to persist before you even think about retrying
Store four fields from the create response, next to the attempt:
- The Message SID
Toand the body hash, so you can see a duplicatestatusnum_segments
If a later attempt has the same To and the same body, and a SID already exists, stop. The customer may still be reading the first text. A second code invalidates the experience even when the second delivery fails.
API observability for agents is where a spike in creates should page someone. A prompt change that says "also text the backup number" doubles recipients. A retry change that posts again doubles messages per recipient. Those are different graphs. Count creates per To, not a single daily total.
There is no .env file in this project. Swytchcode keeps credentials in its own local store and reads them when a call runs. The account SID and auth token stay in that store. A live command is a request that reached Twilio. Dry runs do not send and do not create a Message.
A policy the retry cannot talk its way around
Block a second send in policy when the arguments match a body you already allowed and the first call returned a SID. Approval on Slack or Telegram, on Business and Enterprise, is the right hold for a broadcast to many numbers. It expires after 48 hours if nobody answers, and a late approval does not send. Free and Pro can allow or deny. On those plans, keep the bulk method off.
Geo permissions on the Twilio account can reject a country you did not enable. That failure is not a cue to try a second From number in a loop. Read the error, fix the permission, and send once.
Write the SID down. Treat a missing HTTP response as unknown, not as failed. Send again only when you know the first create did not return a SID, and send it once.
Related reading
- Human approval for a tool call. The hold to put on a send to many numbers.
- API compliance for agent integrations. What to tell a reviewer about message content and retention.
- API governance tools for AI agents. Why the send method is the surface you limit.
- Best API observability tools for agent traffic. When the local count of SIDs is not enough.
FAQ
Does Twilio dedupe two identical SMS creates?
The Message create documented for POST .../Messages.json does not take an idempotency key. Two POSTs produce two Message resources and two SIDs. Assume both can be delivered and both can be billed.
I timed out. Did the text send?
You do not know yet. Look up whether a SID was stored. If the create returned a SID before the client gave up, the message exists. If you have no SID, check Twilio's message list for that To and body before you POST again.
Why did one text cost more than the last one?
Check num_segments. A longer body, or a body that no longer fits in a single GSM segment, is more than one segment. The price for the destination is on Twilio's pricing page and depends on the country. A second create multiplies the whole message, segments included.
Can a dry run produce a SID?
No. swy exec with --dry-run does not send the request. It does not create a Twilio Message and it does not count as a live command. Only a create that Twilio accepted returns a SID.
More content
Circle API transfers and the idempotency key you must reuse
Circle puts idempotencyKey, a UUID v4, in the transfer body. Reuse that key on a retry. A new key starts a second transfer. Code 1083 means the body changed.
How to query the GDELT API without drowning the agent
GDELT's DOC API needs no key. The default search is three months of HTML, up to 75 articles. Set timespan, maxrecords, and format=json, then wait out a 429.
Bitbucket API tokens and the username that causes a 401
Bitbucket app passwords stopped being the supported path in 2026. API tokens use your Atlassian email, need scopes, and can be limited to one workspace.
