Idempotency
Retry delivery and fleet order requests safely with the Idempotency-Key header, without creating duplicates.
Network errors and timeouts leave you unsure whether a request reached Maxmove. To retry safely, POST /v1/deliveries and POST /v1/fleet/orders require an Idempotency-Key header of 1 to 255 characters. Use a value that identifies the order in your system, for example your order id.
curl -X POST https://api.maxmove.com/v1/deliveries \
-H "x-api-key: $MAXMOVE_KEY" -H "content-type: application/json" \
-H "Idempotency-Key: order-4711" \
-d @delivery.jsonHow retries behave#
For POST /v1/deliveries:
| Situation | Response |
|---|---|
| First successful request | 201 with the new delivery. |
| Retry with the same key and body | 200 with the original delivery and the header Idempotent-Replayed: true. |
| Same key with a different body | 409 idempotency_key_reused. |
| Retry while the first request is still running | 409 request_in_progress. Retry a moment later. |
| Header missing | 400 missing_idempotency_key. |
| Header longer than 255 characters | 400 invalid_idempotency_key. |
For POST /v1/fleet/orders, the header rules are the same, but a retry with the same key answers 201 with the existing order. Fleet orders don't send Idempotent-Replayed.
After a timeout, a network error, or a 5xx answer, retry with the same key and the same body. Don't generate a new key for a retry, or you may create a second delivery.
Idempotency keys belong to one mode: the same key used with a test key and a live key creates two independent resources.
Retry with backoff#
async function createDelivery(body: unknown, idempotencyKey: string) {
for (let attempt = 1; attempt <= 5; attempt++) {
try {
const res = await fetch('https://api.maxmove.com/v1/deliveries', {
method: 'POST',
headers: {
'x-api-key': process.env.MAXMOVE_KEY!,
'content-type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(body),
});
const retryable = res.status === 429 || res.status >= 500 ||
(res.status === 409 && (await res.clone().json()).error?.code === 'request_in_progress');
if (!retryable) return res;
} catch {
// Network error: retry with the same key and body.
}
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 500));
}
throw new Error('Delivery creation did not succeed after 5 attempts');
}import os
import time
import requests
def create_delivery(body: dict, idempotency_key: str) -> requests.Response:
for attempt in range(1, 6):
try:
res = requests.post(
"https://api.maxmove.com/v1/deliveries",
headers={
"x-api-key": os.environ["MAXMOVE_KEY"],
"Idempotency-Key": idempotency_key,
},
json=body,
timeout=30,
)
in_progress = (
res.status_code == 409
and res.json().get("error", {}).get("code") == "request_in_progress"
)
if not (res.status_code == 429 or res.status_code >= 500 or in_progress):
return res
except requests.RequestException:
pass # Network error: retry with the same key and body.
time.sleep(2**attempt * 0.5)
raise RuntimeError("Delivery creation did not succeed after 5 attempts")function createDelivery(array $body, string $idempotencyKey): array
{
for ($attempt = 1; $attempt <= 5; $attempt++) {
$ch = curl_init('https://api.maxmove.com/v1/deliveries');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => [
'x-api-key: ' . getenv('MAXMOVE_KEY'),
'content-type: application/json',
'Idempotency-Key: ' . $idempotencyKey,
],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($raw !== false) {
$json = json_decode($raw, true);
$inProgress = $status === 409 && ($json['error']['code'] ?? null) === 'request_in_progress';
if (!($status === 429 || $status >= 500 || $inProgress)) {
return ['status' => $status, 'body' => $json];
}
}
// Network error or retryable answer: retry with the same key and body.
usleep((2 ** $attempt) * 500000);
}
throw new RuntimeException('Delivery creation did not succeed after 5 attempts');
}For 429 answers, wait at least the number of seconds in the Retry-After header. See Rate limits.
external_id#
external_id on a delivery is your own reference and is unique per mode. It is not a replacement for the Idempotency-Key: a second delivery with the same external_id answers 409 duplicate_external_id, even if the body is identical.
Did this answer your question?