Idempotency
Networks drop. Timeouts happen. 503s happen. Idempotency keys make POSTs safe to retry without creating duplicate shipments.
The Idempotency-Key header
Send a unique Idempotency-Key on every POST — UUID v4 recommended. The first request is processed normally and the result is cached against the key; subsequent calls with the same key return the cached response without re-running the operation.
curl https://imileexpress.com/api/v1/shipments \ -H "Authorization: Bearer mk_test_rh9ABekJfP31cN7sQ8jKvLwY" \ -H "Idempotency-Key: 5f3c7a44-2d2b-4f63-a8b1-9c0f1d8e7b62" \ -H "Content-Type: application/json" \ -d '{ "service_tier_id": "HKASTDGGSTCM", "recipient": {...}, ... }' # Same key + same body → identical response, no duplicate shipment. # Same key + different body → 409 Conflict (sys_code 1409002)
⚠ One key = one order
An Idempotency-Key identifies one logical request — not your account, and not your integration. Every new order needs a new, unique key. Hard-coding a single key in a script or an API client (Postman, Insomnia) is the most common integration mistake we see.
The cache stores failed responses too — with one exception: account-state rejections are never cached. A payload error (say a 400 on an invalid address) is cached, so the same key returns the same 400. But rejections that depend on your account — insufficient balance, credit limit, overdue-invoice freeze — are not cached: once you top up or settle, retrying with the same key re-evaluates your account and the order goes through.
In practice: generate a UUID v4 at the moment you build the request and store it alongside your order record, then reuse it only when retrying that same order. In Postman, set the header value to {{$guid}} so a new key is generated on every send.
Per-endpoint requirement
Not every mutating endpoint needs an Idempotency-Key. Create-style endpoints (which mint a new resource / debit the wallet) require it; endpoints that are naturally idempotent (delete, cancel, rotate) treat it as optional.
| Endpoint | Idempotency-Key | Why |
|---|---|---|
| POST /api/v1/shipments | REQUIRED | Creates a shipment & debits the wallet — retries must not duplicate. |
| POST /api/v1/webhooks | REQUIRED | Creates a new webhook endpoint — retries must not duplicate. |
| DELETE /api/v1/webhooks/{id} | OPTIONAL | Naturally idempotent — deleting twice has the same effect. |
| POST /api/v1/shipments/{id}/cancel | OPTIONAL | Naturally idempotent — cancelling an already-cancelled shipment is a no-op. |
| POST /api/v1/webhooks/{id}/rotate | OPTIONAL | Naturally idempotent — converges on resource state, safe to retry. |
24-hour window
Cached responses live for 24 hours — successes (2xx) and payload-level client errors (4xx) alike. After that, the same key is treated as a fresh request. Two kinds of response are never cached, so retrying with the same key re-runs the operation: server errors (5xx), and account-state rejections — sys_code 1500009 (insufficient available balance), 1500012 (overdue-invoice freeze), 1500013, 1500014 and 1500016 (credit floor / limit).
Conflicts
Same key, different body → rejected with 409 (sys_code 1409002). The error message includes a digest of the original body so you can identify the mismatch.