A customer places an order. The ERP creates it, but the connection drops before the ecommerce platform receives confirmation.
The ecommerce platform retries the request.
Should the ERP create another order?
This is a common challenge in retail integration. A timeout tells the client that it did not receive a response. It does not tell the client whether the operation succeeded.
This article explains how to design idempotent retail APIs that handle retries without creating duplicate orders.
The Core Problem: One Order, Multiple Requests
Retail transactions often move between several systems:
- Ecommerce storefronts
- Point-of-sale applications
- ERP platforms
- Payment providers
- Fulfillment services
Each connection can experience delays, interrupted responses, or temporary failures.
Consider this sequence:
- An ecommerce platform sends an order to the ERP.
- The ERP creates order
ORD-10482. - The response is lost during a network interruption.
- The ecommerce platform sends the request again.
- The ERP creates order
ORD-10483.
The customer submitted one order, but the system recorded two.
Possible consequences include duplicate inventory reservations, fulfillment requests, customer notifications, and accounting entries.
Retries help systems recover from failures. They also need a way to recognize work that has already been completed.
What Makes an API Idempotent?
An operation is idempotent when repeating it produces the same intended effect as performing it once.
For order creation, the desired behavior is straightforward:
Repeated requests for the same order operation should create one order.
A common approach is an idempotency key: a client-generated identifier sent with the request. Stripe documents this pattern for safely retrying operations and checking whether a reused key carries different parameters.
For example:
POST /api/v1/orders
Content-Type: application/json
Idempotency-Key: 8e491173-c2dc-49a5-9e71-7280135ec26a
{
"externalOrderId": "WEB-58219",
"locationId": "STORE-004",
"items": [
{
"sku": "SHOE-BLK-042",
"quantity": 1
}
]
}
The client generates the key once, persists it with the pending operation, and reuses it during retries.
A separate order operation receives a new key.
Define the Request Contract
Before implementing storage or retry logic, define how the endpoint should behave.
For this example, the API uses the following contract:
| Request condition | API behavior |
|---|---|
| New key and valid payload | Create the order and save the response |
| Same key and same payload | Return the saved response |
| Same key and different payload | Return 409 Conflict
|
| Concurrent request using the same key | Wait briefly for completion or return a documented retryable conflict |
These are design choices for this endpoint, rather than universal rules for every API.
The server should also scope keys by the authenticated tenant and operation. A key used by one retailer must not retrieve another retailer’s response.
Authentication and authorization still apply when returning a saved result.
Store the Key with the Result
An idempotency record needs enough information to identify the operation, compare requests, and recover its result.
A simplified PostgreSQL table could look like this:
CREATE TABLE api_idempotency (
tenant_id UUID NOT NULL,
operation TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
request_hash TEXT NOT NULL,
response_status INTEGER,
response_body JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (
tenant_id,
operation,
idempotency_key
)
);
The request_hash represents the validated, normalized request.
For JSON payloads, use deterministic serialization before hashing. Equivalent objects can arrive with different property ordering.
If a retry changes the quantity or delivery details while reusing the original key, the server should reject it rather than silently return an unrelated result.
Handle Concurrent Requests Safely
A basic implementation might:
- Check whether the key exists.
- Create an order if it does not.
- Save the key afterward.
Two requests can both pass the first check before either saves the key.
A separate lookup is not enough to prevent this race.
Use a database uniqueness constraint to coordinate competing requests. PostgreSQL supports INSERT ... ON CONFLICT for handling conflicts with unique constraints.
For example:
INSERT INTO api_idempotency (
tenant_id,
operation,
idempotency_key,
request_hash
)
VALUES ($1, 'create_order', $2, $3)
ON CONFLICT DO NOTHING
RETURNING idempotency_key;
The insert result determines which request owns the operation. A conflicting request follows the replay path instead of creating another order.
Keep Order Creation and Response Storage Atomic
Concurrency protection solves only part of the problem.
What happens if the server creates the order and crashes before saving the idempotency response?
For an order stored in the same database, use one transaction to:
- Claim the idempotency key.
- Create the order.
- Save the response.
- Record any downstream work in an outbox.
- Commit the changes together.
The simplified pseudocode below shows how the API handles retries without creating duplicate orders:
Authenticate, authorize, and validate the request
BEGIN TRANSACTION
Attempt to insert the scoped idempotency key
If the key already exists:
Read the committed record
Reject if the request hash differs
Otherwise return its saved result
Create the order
Save the response status and body
Insert an order-created event into the outbox
COMMIT
Return the saved response
This example assumes PostgreSQL’s default READ COMMITTED isolation. Lock timeouts and transaction failures need a documented retry response.
If the transaction rolls back, neither the order nor its idempotency record is committed. If it commits but the HTTP response is lost, a retry can retrieve the saved result.
Keep remote network calls outside this database transaction.
Protect Downstream Operations Too
Creating one local order does not automatically prevent duplicate work elsewhere.
The order might trigger:
- Payment processing
- Inventory reservation
- ERP synchronization
- Fulfillment
- Customer notifications
An outbox worker can publish committed events, but a crash may cause the same event to be delivered again.
Each consumer therefore needs its own duplicate protection, such as a unique event identifier recorded atomically with its local changes.
For remote operations, pass a stable operation identifier to downstream systems that support idempotency. When a remote outcome is uncertain, reconcile its status before starting a replacement operation.
The goal is to make each business effect safe to repeat within its own boundary.
Plan for Offline POS and Delayed Retries
Retail retries may arrive seconds later—or after a store reconnects the following day.
For offline POS transactions:
- Generate the transaction identifier before transmission.
- Store it durably with the local transaction.
- Preserve it across application restarts.
- Reuse it during synchronization.
Also distinguish an API idempotency key from a business order reference.
A key identifies a particular API operation. A source order reference identifies the order in the originating system.
Where business rules require uniqueness, enforce a constraint such as:
UNIQUE (tenant_id, source_system, external_order_id)
This adds protection when a client accidentally sends the same business order with a new key.
Define how the API handles an existing reference, including conflicting order details.
Test Failure Scenarios
A successful request followed by an immediate retry is only the starting point.
Useful scenarios include:
- The response disappears after the transaction commits.
- Two identical requests arrive simultaneously.
- The same key arrives with a different payload.
- The database transaction rolls back.
- An offline POS resends a transaction after restarting.
- An outbox event reaches a consumer twice.
Verify the resulting business state: one order, the expected inventory effects, and no duplicate downstream work.
Logs should connect the request, scoped idempotency key, source order reference, and internal order ID without exposing sensitive payloads.
Final Thoughts
Retail integrations need retries because networks and services fail.
Idempotent API design makes those retries predictable through stable operation identifiers, database constraints, atomic writes, and downstream duplicate protection.
For an order-creation endpoint, the practical objective is simple: when the client asks again after an uncertain outcome, the system can identify the original operation and respond without creating another order.
For developers working with retail ERP and POS systems, reliable retry handling is an important integration requirement.
How does your integration handle a timeout after the ERP has already committed the order?
Top comments (0)