# 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.

```bash
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.json
```

## How 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

```ts Node.js
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');
}
```

```python Python
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")
```

```php PHP
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](https://maxmove.com/en/developers/docs/concepts/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.

---

Source: https://maxmove.com/en/developers/docs/concepts/idempotency
