SMS

How do I avoid sending the same SMS twice?

The duplicate you can actually prevent is the one you cause: a request that timed out, was retried, and turned out to have succeeded the first time. That is what an idempotency key is for, and on SMS it matters more than on most operations because the side effect is a message a person reads, and it cannot be taken back.

How does the key work?

Send an Idempotency-Key header with a unique value for each logical send, and reuse that same value for every HTTP attempt of it. If a request succeeds without returning a response, replay the identical request with the same key and Bird returns the original result rather than sending a second message.

The word doing the work is "logical". The key identifies the thing you meant to do, not the HTTP call you happened to make. One order confirmation is one key, however many times the network makes you ask for it.

A replay is visible rather than silent: the cached response comes back with an Idempotency-Replay: true header, so your own logs can tell a first send from a replay.

Keys are scoped to your workspace, and a completed response is kept for three hours. After that window a retry with the same key is processed as a fresh request, because no deduplication record remains. That window comfortably covers ordinary retry schedules, but it is a real horizon: a job that retries a day later is not protected by the key it used yesterday.

What are the failure responses telling me?

Three of them, and they mean quite different things.

  • 409 with E01005 IdempotencyKeyReuse: the same key with a different request. Bird refuses immediately rather than handing you a response that does not match what you sent. This is a bug in your key generation and should not be retried. The comparison covers the method, the endpoint, the path and query parameters, and the raw body, so even a whitespace change counts as a different request.
  • 409 with E01004 RequestInProgress: a concurrent request with the same key has not finished. Usually an aggressive client timeout retrying while the first attempt is still running. The in-flight lock expires within 30 seconds, so wait briefly and retry.
  • A 5xx or a timeout: retry it, with the same key. 5xx responses are never cached, deliberately, because a server error means Bird does not know whether the request took effect and caching it would permanently block a successful retry. You then get the original successful response if the first attempt completed, or the result of the new one.

That last rule is the one worth internalising. The instinct on a 5xx is to generate a fresh key so as not to be blocked. Doing that is what actually sends the message twice.

Does the key guarantee no duplicates?

No, and the gap is documented rather than hidden.

If the deduplication store is briefly unavailable, requests proceed without deduplication. They are not refused. That is the right trade for a messaging API, since refusing every send because a cache is unreachable is a worse outage than an occasional repeat, but it means the key reduces duplicates rather than eliminating them.

So design for it. Make a repeated message survivable: include an order or reference number the recipient can recognise, avoid phrasing that reads as a second distinct event, and where a duplicate would be genuinely harmful, keep your own record of what you have sent rather than relying only on the header. The three-hour window is the other half of the same advice, because anything retried outside it has no protection at all.

What about a message the handset shows twice?

That is a different question with a different answer, and the key cannot help with it.

Everything above is about sending once. A message that was sent once and appears twice on the recipient's phone is a delivery-side event: carrier retries, and the reassembly of a multi-segment message, are the usual causes, and neither is visible from your side of the API. If your evidence is a screenshot from a recipient rather than two accepted sends in your logs, look there rather than at idempotency.

The way to tell them apart is your own send log. Two message IDs means you sent twice, and the guidance on this page applies. One message ID means you did not, and the duplicate happened downstream.

What should I do?

  1. Generate one key per logical operation and reuse it for every attempt of that operation.
  2. Retry network errors, timeouts and 5xx with backoff, reusing the key each time.
  3. Treat 409 IdempotencyKeyReuse as a defect to fix, never as something to retry around.
  4. Skip keys where a duplicate is harmless. The mechanism exists for the sends where it is not, and using it everywhere makes the cases that matter harder to see.

Idempotency covers the key format, the retention window and the full failure table, and sending SMS covers the send itself.

Build on the same network.

A test API key is yours immediately. Production unlocks when you add a payment method and verify a sender.

Start with one channel.
Add the others when you're ready.

A test API key is yours immediately. Production unlocks when you add a payment method and verify a sender.

Using Claude Code, Cursor, or Codex? Copy a setup prompt and your agent installs the Bird CLI and skills for you. Pick yours:

Cursor