Idempotency (Idempotency-Key)
How to use the Idempotency-Key header so that retries from your integrations do not duplicate purchase orders or apply a receipt twice.
If you integrate Mosce ERP with an external system (a third-party ERP, your business's accounting service or your own scripts), the
Idempotency-Keyheader guarantees that a retry after a network timeout, a redeploy or a double submission does not create the same operation twice. The server recognizes that it already processed that operation and returns the original response.
Reading time: ~6 min
Do you use Mosce ERP only from the web interface? Then you do not have to do anything: the application generates and sends the key automatically. This article is for integrators who call the API directly.
What it is and why it matters
Some API operations mutate data (create a purchase order, send it, receive it, cancel it). If your integration sends the request and the network drops before receiving the response, you do not know whether the operation completed. Retrying blindly could create a second order or apply a receipt twice.
The Idempotency-Key header solves this: it identifies a logical attempt by the user. If you retry with the same key, the server returns the result of the first execution instead of running again.
Endpoints that require the header
These mutation endpoints of the purchasing module require Idempotency-Key:
POST /api/v1/purchases- create purchase order.POST /api/v1/purchases/{id}/send- send the order to the supplier.POST /api/v1/purchases/{id}/cancel- cancel the order.POST /api/v1/purchases/{id}/receive- receive goods.POST /api/v1/purchases/{purchaseId}/receipts/{receiptId}/emit-e41- emit the receipt document.
Grace period for existing integrations: if an integration does not yet send the header, its next call to these endpoints will fail with
400 IDEMPOTENCY_KEY_REQUIRED. Coordinate with your integrator to add it before the next major version.
How to generate the key
- Format: a URL-safe string of 16 to 128 characters using the alphabet
[A-Za-z0-9_-]. - Recommended: a UUID v4 (
crypto.randomUUID(), 36 characters), a UUID v7 (sortable), a ULID (26 characters) or a random base64url token. All are valid. - One key per logical attempt. Generate a new key for each distinct operation. Reuse it ONLY when you retry exactly the same operation (for example, after a timeout).
- Do not use predictable or short values: fewer than 16 characters of the URL-safe alphabet leaves too little entropy and opens the risk of accidental collision between concurrent operations.
The possible responses
| Situation | Response |
|---|---|
| First time with that key | The server runs the operation and returns its normal result. |
| Retry with the same key and the same body | The server returns the original response (same code and body) without running again. This is the "exactly once" guarantee. |
| Same key but different body | 422 IDEMPOTENCY_KEY_BODY_MISMATCH - you are recycling the key for another operation. Generate a new key. |
| Two concurrent retries (same key, same body) | One runs; the others wait up to 30 s for it to finish and then receive the same response. If the first one hangs for more than 30 s, the others receive 409 IDEMPOTENCY_KEY_TIMED_OUT and can retry later. |
Critical requirement: the body must be byte-identical
The server computes a fingerprint of the raw JSON body and compares it against the one from the first submission. Therefore, on a retry, the body must be serialized byte for byte identical to the original:
- Same property order (or use a canonical serialization).
- Same spacing.
- Same numeric representations -
5and"5"produce different fingerprints even if the server later interprets them the same.
If you cannot guarantee identical bytes between retries, then it is not a real retry: generate a new key (it is a new operation).
Error codes
| Code | Meaning | What to do |
|---|---|---|
IDEMPOTENCY_KEY_REQUIRED (400) | The header is missing on an endpoint that requires it. | Add Idempotency-Key. |
IDEMPOTENCY_KEY_INVALID_FORMAT (400) | The value is outside the format (16 - 128 URL-safe characters). | Generate a valid value (UUID/ULID). |
IDEMPOTENCY_KEY_BODY_MISMATCH (422) | You reused the key with a different body. | Use a new key per new operation. |
IDEMPOTENCY_KEY_TIMED_OUT (409) | Another concurrent retry took too long. | Retry later; the system releases the lock automatically. |
IDEMPOTENCY_REQUIRES_TENANT (400) | The request did not resolve a valid tenant. | Check your credentials/token. |
Retention window
The key stays associated with its result for 24 hours from first use. After that time it is released and could be reused - but the recommendation is to always generate a new key per operation.
Example with curl
curl -X POST https://api.your-domain.com/api/v1/purchases \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 1f2c4a9e-7b3d-4e21-9a6c-8d5f0b2e1c34" \
-d '{"supplierId":"sup_123","items":[{"productId":"prd_9","orderedQuantity":10,"unitCost":11000}]}'If the call fails due to a timeout, repeat exactly the same request (same Idempotency-Key and same body): you will get the original result without creating a second order.
Example with fetch (JavaScript)
const idempotencyKey = crypto.randomUUID();
const body = JSON.stringify({
supplierId: 'sup_123',
items: [{ productId: 'prd_9', orderedQuantity: 10, unitCost: 11000 }],
});
async function createPurchase() {
return fetch('https://api.your-domain.com/api/v1/purchases', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey, // reuse the SAME value and the SAME body when retrying
},
body, // reuse the SAME exact string
});
}Related
- Set up a webhook
- Reliability and retries
- Purchases - the operations these endpoints cover.