Content

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.

EngineeringOct 30, 2026

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
  • To and the body hash, so you can see a duplicate
  • status
  • num_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.

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