Idempotency
The Idempotency-Key header, optional but recommended, replays an interrupted request with no risk of duplicate charges.
Networks fail. Idempotency guarantees that retrying an interrupted request never duplicates the operation or the charge.
You generate the key
The idempotency key is created by you, client-side. The API never issues one. Before you send the request:
- Generate a unique value for that business operation. Recommended: a UUID v4 (
crypto.randomUUID()in Node.js,uuid.uuid4()in Python) or any string with enough randomness to never repeat (up to 255 characters). - Send it in the
Idempotency-Keyheader. - Store it next to your operation (the order, the job) so every retry uses exactly the same key.
One key represents one business attempt: the same key for that operation’s retries, a fresh key for each distinct operation.
How it works
Every mutation (POST, PUT, PATCH) accepts an Idempotency-Key header (recommended: UUIDv4, up to 255 characters):
curl -X POST https://api.sendit.mx/v1/shipments/clxq1w2e3r4t5y6u7i8o9p0a/label \
-H "X-API-Key: sk_test_..." \
-H "Idempotency-Key: 1f0e6f0e-59a4-4a6f-9d2e-9b1a7b2c3d4e" \
-H "Content-Type: application/json" \
-d '{ "rateId": "ESTAFETA_economy_x9y8z7" }'
Retrying with the same key within a 24-hour window replays the original result: the same status code and the same body, whether it was a success or a failure. The response sets Idempotent-Replayed: true and echoes your key back.
Two important nuances:
- Validation errors (
400) are never stored: fix the payload and reuse the same key freely. - Any other replayed failure (e.g. a
402) keeps replaying: after resolving the cause, retry with a new key.
Where it matters most
The key is optional on every endpoint. If you do not send one, the request runs normally with no replay protection. You never get a 400 for a missing key. Send it above all on the operations that move money:
| Endpoint | Operation |
|---|---|
POST /v1/shipments/:id/label |
Label purchase |
POST /v1/shipments/:id/label/void |
Label void |
POST /v1/wallet/fund/card |
Card funding |
POST /v1/wallet/fund/paypal |
PayPal funding |
POST /v1/shipments with purchase |
One-call buy — replays the create and the purchase as a single unit |
On funding calls, the key also guarantees that retries reuse the same payment intent. Your card is never charged twice.
Your balance is protected even without a key. The wallet de-duplicates every movement server-side, so retrying an interrupted purchase or funding call never double-charges. The key adds a replay of the original result, with the same status and the same body. That is why we recommend it on every money flow.
Trackers: POST /v1/trackers does not use idempotency and does not need it. Registering the same number twice returns the existing tracker, with no double charge.
Conflicts
| Code | When | How to resolve it |
|---|---|---|
409 IDEMPOTENCY_KEY_REUSED |
Key reused with a different body or endpoint | Generate a fresh key for each distinct operation |
409 IDEMPOTENCY_KEY_IN_USE |
A concurrent request with the same key is still in flight | Wait a moment and retry with the same key |
Recommended pattern
Generate the key before the first request and store it next to your business operation, such as the order or the fulfillment job. Any retry then uses the same key, whether it is immediate or comes after your process restarts:
// When creating your internal order, generate and store the key
const idempotencyKey = crypto.randomUUID();
await db.orders.update(orderId, { senditIdempotencyKey: idempotencyKey });
// Every retry reuses the same key
const res = await fetch(`https://api.sendit.mx/v1/shipments/${shipmentId}/label`, {
method: "POST",
headers: {
"X-API-Key": process.env.SENDIT_API_KEY,
"Idempotency-Key": idempotencyKey,
"Content-Type": "application/json",
},
body: JSON.stringify({ rateId }),
});