Prevent duplicate SMS and MMS
By Ali Gasimzade, Senior Engineer at Blocx.
A customer places one order and gets three confirmation texts. Nobody designed that experience. A timeout, a retrying worker or a repeated webhook turned one business event into several sends. We have seen teams run into this while connecting their backends to messaging APIs, including Twilio. The integration worked on the happy path; its retry behavior was the part that got missed.
A timeout is not proof that nothing happened
Your application submits a message. Blocx accepts it, but the response never reaches your server. Your worker sees a timeout and tries again. Without a shared identity for those attempts, that retry can become another message—and another text to the same customer.
Longer timeouts do not solve this completely. Queue redeliveries, concurrent workers and application restarts can all repeat work. Your backend needs to distinguish a new message from another attempt to submit the original one.
One intended message. One persistent key.
Blocx supports the optional Idempotency-Key header on POST /messaging/send for SMS, MMS and sandbox sends. It lets your application retry the same submission without creating another message. Email and 2FA verification requests do not currently support this header.
Create the key once for each intended message and recipient, then save it with the original request body before sending. A UUID saved on your sending job works, as does a stable event-and-recipient identifier. Never generate a fresh UUID inside your retry loop.
- Same message, same recipient, another attempt: reuse the original key and body.
- A different recipient or a new notification: create a new key.
- An intentional repeat of identical content: it is a new message, so it needs a new key.
- Include the key on the first attempt. Adding one after an unkeyed timeout cannot deduplicate the earlier request.
Add the header to your send
Use your normal authentication headers and include the message's saved key. This example uses an order event and recipient identifier:
curl -i https://api.otterblocx.com/messaging/send \
-H "x-access-key-id: $BLOCX_ACCESS_KEY_ID" \
-H "x-secret-access-key: $BLOCX_SECRET_ACCESS_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: order-1042-dispatched-recipient-17' \
-d '{
"from": "+15551234567",
"to": "+15557654321",
"body": "Your order has shipped.",
"type": "SMS"
}'Replace the example numbers with your provisioned sender and intended recipient, or follow the sandbox guide for a test send. Use a different key for each new notification. If this submission times out, retry it with the same key and unchanged body.
What comes back on a replay
Once the original request completes, Blocx retains its HTTP status and response body. A retry returns that original response with Idempotency-Replayed: true and the same message identity. It does not create another message, reserve funds again or consume sending quotas again. Each HTTP attempt still gets its own X-Request-Id for troubleshooting.
When you receive 202 Accepted, save the returned messageId and stop submitting that message. Acceptance is asynchronous, not proof of handset delivery. Follow delivery through message history or webhooks. Replaying a 202 returns the original acceptance receipt, not the message's latest delivery status.
Retry carefully—not blindly
Read the response code as well as the HTTP status. Several different situations return 409, and they do not all call for another retry:
- 400 invalid_idempotency_key: correct the key format; this request did not start a send.
- 409 idempotency_conflict: the key belongs to different content. Recover the original body to retry it; do not silently substitute a new key.
- 409 idempotency_in_progress: wait for Retry-After seconds, then retry the same key and body.
- 503 send_outcome_unknown, a timeout or a lost connection: acceptance is uncertain. Retry only the same key and body, or look up the message.
- 409 send_outcome_unknown: the interrupted preparation needs investigation. Contact support with the message ID and request ID instead of creating a replacement send.
- A definitive pre-send rejection, such as 402 or 429: fix the cause, then use a new key for the corrected attempt. The old key keeps replaying its rejection.
A timeout or a 5xx response alone does not establish that nothing was sent. If you have the message ID, check GET /messaging/sends/{messageId}. A 404 is not proof of non-delivery either: the record could be unavailable, expired or older than this feature. Keep the original job and ask support to investigate unresolved outcomes.
Keep the key useful
Keys are case-sensitive and must contain 1–128 printable ASCII characters without spaces. The namespace is shared across all API credentials for one account, so rotating credentials does not make a previously used key new.
A key binds the validated sender, recipient, message type, text, media URLs and tags. Keep that content unchanged on retries. JSON whitespace and property order do not matter, and omitted defaults such as type: "SMS" are normalized.
Records are retained for at least 90 days from the first request. Retrying does not extend that period. Never recycle an old key for a new message or depend on deduplication after the retention window.
Fewer accidental sends. A better product.
This protection does not replace a well-designed backend. You still need persistent jobs, deliberate retry rules and webhook handlers that tolerate repeated events. It gives those systems a safer API boundary when the same message submission needs another attempt.
If blocking duplicate sends means Blocx makes less money, we are fine with that. You should pay for the messages you meant to send—not for a retry loop you did not mean to ship. Fewer unnecessary texts and fewer awkward customer conversations make this a better product.
Read the full idempotency contract and sandbox guide before integrating.
Brand identity
Blocx is the programmable messaging product from OtterBlocx, LLC, headquartered in Cheyenne, Wyoming, United States.
Channels
- Live today: SMS, MMS, RCS, Email, Telegram.
- Blocx also offers separate Voice services: VoIP, voicemail, call routing, 2FA via AI caller, and a voice dialer. These are not channels on the messaging or Verify API.
Plans
- Starter — self-serve with $25 in welcome credit for new accounts. Credit covers your first brand, campaign, phone numbers and messages. Paid Starter is $49/month credited toward usage. SMS $0.0065/segment, email $0.001/email (1 sending domain), 1 API key. No MMS, RCS, Telegram, SHAFT or restricted verticals.
- Scale — $1,000/month minimum platform spend (starts on day 31; no minimum for the first 30 days), on an annual contract. Every channel and route type at the published rate card — all rates listed on this site are Scale rates. Signup starts with a call. Required tier for SHAFT and other restricted verticals.
- Volume — custom volume-based pricing negotiated on your traffic profile. Contact sales.
Published rate card (+ carrier passthrough on SMS/MMS)
- SMS: 10DLC $0.0045, toll-free $0.0055, short code $0.007.
- MMS: 10DLC $0.0135, toll-free $0.016, short code $0.018.
- RCS: $0.0135/msg + one-time $1,000 setup fee.
- Email: $0.0002 per email, flat.
- Verify: $0.035 per successful verify, flat.
- Telegram: $0.0045/msg, flat.