Idempotency Keys in an SMS API: How to Prevent Duplicates During Retries and Timeouts
An idempotency key can help manage HTTP request retries, but only if the API defines its scope and behavior. Learn what to agree on before retrying a send and how to distinguish request acceptance from delivery status.

What problem does idempotency solve when sending SMS?
A client can send an HTTP request and lose its connection before receiving the response. At that point, it may not know whether the server processed the request. If it blindly retries a send that the API treats as a new operation, it may cause a second acceptance and potentially a duplicate message.
An idempotency key is a mechanism an API can offer to recognize that multiple requests correspond to the same operation. Its usefulness depends on the specific API contract: the HTTP standard does not itself define an idempotency key or its rules for an SMS-sending endpoint.
- Goal: prevent a retry of the same operation from being treated as a new request.
- Limitation: preventing duplicate acceptance is not the same as guaranteeing that a message is delivered only once.
- Before implementing retries, check the API documentation to see whether it supports keys, what their scope is, and what response it returns when they are reused.

Repeating an HTTP request does not always mean repeating the same send
HTTP semantics distinguish between idempotent and non-idempotent methods. According to RFC 9110, an operation is idempotent when making several identical requests has the same intended effect on the server as making one. The specification identifies methods such as PUT and DELETE; it does not classify POST as idempotent by default.
Therefore, do not assume that resending an SMS-sending POST is safe. If the connection is interrupted before a response arrives, the client may not know whether the request was executed. RFC 9110 advises against automatically retrying a non-idempotent operation unless its semantics are known to be idempotent or it can be determined that the original request was not applied.
- A timeout describes what the client observed, not necessarily what happened on the server.
- A received rejection is different from a lost response: if the API returned a response, use its status code and contract to decide what to do next.
- Do not turn every HTTP error, connection closure, or timeout into an automatic resend.

Generate a stable key for each business operation
The key should identify a logical operation that the system can recognize across all its attempts, such as an OTP request associated with a specific internal event. If a new key is generated for every retry, the API will have no shared identifier to link the requests.
The key should not be constructed using unnecessary personal data or secrets. Define how it is generated and stored within your system, and avoid reusing it for a different operation. The available sources do not specify an exact key-generation method, format, or entropy: these should be agreed on based on the API documentation and your integration’s security requirements.
- Assign a key when creating the business operation, before the first HTTP attempt.
- Keep the same key for retries of that operation.
- Create a different key for a new operation, even if the recipient and content are the same.
- Do not include credentials, tokens, or personal information unless necessary.
Define the scope and duration before relying on the key
A key is unambiguous only within the scope established by the API. The contract should clarify whether it is interpreted per account, endpoint, operation, or some other combination, as well as how long the association between the key and request is retained. There is no universally established retention period for SMS APIs.
If the client retries after the API has stopped retaining the key, the server may treat the request as new. The retention window should therefore cover the period during which your system may need to recover a lost response, and the application should know what to do when that period expires.
- Confirm the key’s uniqueness scope; do not assume it is global.
- Document the retention window and what happens after it expires.
- Align the client’s retry period with the retention period guaranteed by the contract.
- If the API does not document these points, ask for clarification before automating resends.
Agree on what happens if the same key is used with a different payload
Reusing a key with different data is a critical case. The contract should specify how the API compares requests and what happens when the same key is received with an incompatible payload. A prudent policy is for the integration not to reuse a key to change the recipient, content, or other fields that alter the operation; the specific response to the conflict should be confirmed in the service documentation.
It is also useful to know what result the client receives when it repeats the exact same request. Some APIs may return a result associated with the first operation, but there is no available evidence to say that they all do. Do not assume that the original response will be replayed or that a specific conflict status code will be used without confirmation from the provider.
- Keep the payload associated with a key unchanged across retries.
- Define which fields make up the operation’s identity.
- Verify the behavior when a key is reused with a different payload.
- Record the response received without treating it as proof of delivery to the handset.
Design how to handle timeouts and lost responses
When no response arrives, the first decision is not simply to resend: determine whether the API offers a safe way to repeat the request using the same key or to look up the operation. If there is no idempotency contract or mechanism for finding out what happened, the status may remain indeterminate; a new POST could create another operation.
Separate errors for which a response was received from cases where no response arrived. For the latter, use only the options documented by the API. Do not treat a timeout as proof that the server did not execute the request.
- Save the key and the data needed to correlate the attempt before sending the request.
- After a lost response, retry using the same key only if the contract confirms it is safe.
- If an operation or status lookup is available, use it before creating a new send.
- If there is no documented way to resolve the uncertainty, avoid blindly resending and handle the case as indeterminate.
Control concurrent requests and persistence
Two processes may try to send the same operation at the same time—for example, if a queue delivers a job again while another worker is still processing it. The integration should prevent local concurrency from generating different keys for the same business event or losing the link between the key and payload.
The available evidence does not define a universal atomic storage method or a specific locking mechanism for an SMS API. Design controls in your application layer and confirm how the API handles simultaneous requests with the same key. Do not assume it deduplicates concurrent races unless the contract says so.
- Persist the key and operation identity before dispatching the send.
- Ensure concurrent workers retrieve the same key for the same operation.
- Define which local record takes precedence if two processes try to create the operation at once.
- Test simultaneous requests and verify the endpoint’s documented behavior.
Acceptance does not confirm the message’s final status
When supported by the API, an idempotency key handles repeated requests within a defined contract. It does not confirm that the message reached the handset or replace status tracking. Keep the HTTP request result separate from later message information in your data model and reports.
If the request was accepted but the final status is not yet known, retain the identifiers and correlation data returned by the API and use its documented lookup mechanisms. Do not create a new send just because the status has not updated yet. A received DLR should not be presented as independent verification of receipt on the handset unless such verification is available.
- Record the idempotency key, HTTP result, and available message identifiers separately.
- Look up or reconcile statuses using the capabilities documented by the API.
- Do not confuse acceptance, a status reported by the route, and independently verified receipt.
- Do not promise end-to-end single delivery based solely on API idempotency.
Frequently asked questions
Does an idempotency key guarantee that an SMS will be delivered only once?
No. It may help prevent an API from accepting the same operation more than once, if the service defines and applies that contract. It does not prove receipt on the handset or guarantee end-to-end single delivery.
Should I retry an SMS send when a timeout occurs?
Not blindly. A timeout does not prove that the server did not process the request. Retry with the same key only if the API documents that behavior, or look up the operation using a documented mechanism.
What happens if I use the same key with a different payload?
It depends on the API contract. There is no universally established rule for SMS APIs. Keep the payload unchanged for each key and confirm how the service responds to an incompatible request.
How long should a key be retained?
The duration depends on the API. Confirm its retention window and align your client’s retry period with it; do not assume there is a standard duration.
Does a DLR confirm that the user received the message on their phone?
It should not automatically be treated as independent proof of receipt on the handset. Preserve the distinction between a reported DLR status and independent verification, if available.
Sources consulted
- HTTP Semantics (RFC 9110)IETF
- SMPP Protocol Specification v3.4SMPP Developers Forum