Idempotency
The Idempotency-Key header: safe write retries without duplicates.
Networks are unreliable: a response can be lost after the server has already processed the
request. Simply repeating the POST would create a duplicate listing. Send an Idempotency-Key
header to prevent that.
curl -X POST "https://api.samo-trans.com/api/public/v1/proposals/cargo" \
-H "Authorization: Bearer $SAMO_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-88213-attempt-1" \
-d '{ "startLocations": [ … ], "endLocations": [ … ], … }'
The header is optional and applies only to writes (POST, PATCH, DELETE). It is ignored
on GET, which is safe to repeat anyway.
Choosing a key
The key must be stable for one logical operation and different across operations. The most reliable source is an identifier from your own system:
Idempotency-Key: tms-order-88213
Do not generate a random string at retry time — the retry would then look like a brand new operation and the header would achieve nothing. Generate the key once, when you decide to perform the operation, and reuse it for every attempt.
Behaviour
| Situation | Result |
|---|---|
| First request with the key | Runs normally; the response is remembered for 24 hours |
| Retry: same key, same body | The stored response is returned. No second listing is created |
| Retry: same key, different body | 422 with code validation_error — that key already belongs to another operation |
| Retry while the first request is still running | 409 with code conflict — try again in a moment |
The in-flight protection window is 60 seconds. If the first request finished sooner, the next one immediately receives the stored response.
The key is scoped to one API key, method and path: POST /proposals/cargo and
POST /proposals/transport with the same Idempotency-Key are two different operations.
A retry loop worth copying
async function createWithRetry(payload, idempotencyKey) {
for (let attempt = 0; attempt < 5; attempt += 1) {
const res = await fetch(`${API_BASE}/api/public/v1/proposals/cargo`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SAMO_KEY}`,
"Content-Type": "application/json",
// the same key on every attempt — that is the whole point
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(payload),
});
if (res.ok) return res.json();
const { error } = await res.json();
// 409 — a previous attempt is still in flight; 429 — rate limited. Both are worth retrying.
if (error.code === "conflict" || error.code === "rate_limited") {
const retryAfter = Number(res.headers.get("Retry-After") ?? 0);
await sleep(retryAfter * 1000 || 2 ** attempt * 500);
continue;
}
throw new Error(`${error.code}: ${error.message} (requestId ${error.requestId})`);
}
throw new Error("giving up after 5 attempts");
}
Together with batch creation
POST /proposals/batch supports Idempotency-Key too, and that is where it matters most: a
retry after a dropped connection will not create 25 duplicates — it returns the stored result of
the first attempt along with the outcome of each item.