<!-- Carebit docs: Idempotency and retries -->

# Idempotency and retries

Every `POST` create and `PATCH` update endpoint in the Developer Platform
API requires an `Idempotency-Key` header. Sending the same key twice with
the same request body returns the stored response, so a network retry
never creates duplicate resources.

## Sending the key

Generate one UUID per logical operation (not per network attempt). Send
it on the initial request and on every retry of that operation.

<!-- code-tabs -->

### cURL

```bash
IDEMPOTENCY_KEY=$(uuidgen)

curl -X POST "https://api.carebit.co/v1/bookings" \
  -H "Authorization: Bearer $CAREBIT_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{
    "patient_id": "00000000-0000-4000-8000-000000000004",
    "start_time": "2026-01-01T09:00:00Z",
    "service_id": "00000000-0000-4000-8000-000000000005",
    "service_variant_id": "00000000-0000-4000-8000-000000000006"
  }'
```

### JavaScript

```javascript
const response = await fetch("https://api.carebit.co/v1/bookings", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CAREBIT_ACCESS_TOKEN}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    patient_id: "00000000-0000-4000-8000-000000000004",
    start_time: "2026-01-01T09:00:00Z",
    service_id: "00000000-0000-4000-8000-000000000005",
    service_variant_id: "00000000-0000-4000-8000-000000000006",
  }),
});
const booking = await response.json();
```

### Python

```python
import os
import uuid
import requests

response = requests.post(
    "https://api.carebit.co/v1/bookings",
    headers={
        "Authorization": f"Bearer {os.environ['CAREBIT_ACCESS_TOKEN']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "patient_id": "00000000-0000-4000-8000-000000000004",
        "start_time": "2026-01-01T09:00:00Z",
        "service_id": "00000000-0000-4000-8000-000000000005",
        "service_variant_id": "00000000-0000-4000-8000-000000000006",
    },
)
booking = response.json()
```

### Ruby

```ruby
require "httparty"
require "json"
require "securerandom"

response = HTTParty.post(
  "https://api.carebit.co/v1/bookings",
  headers: {
    "Authorization" => "Bearer #{ENV.fetch("CAREBIT_ACCESS_TOKEN")}",
    "Content-Type" => "application/json",
    "Idempotency-Key" => SecureRandom.uuid
  },
  body: {
    patient_id: "00000000-0000-4000-8000-000000000004",
    start_time: "2026-01-01T09:00:00Z",
    service_id: "00000000-0000-4000-8000-000000000005",
    service_variant_id: "00000000-0000-4000-8000-000000000006"
  }.to_json
)
booking = response.parsed_response
```

### PHP

```php
$client = new GuzzleHttp\Client();

$response = $client->post("https://api.carebit.co/v1/bookings", [
    "headers" => [
        "Authorization" => "Bearer " . getenv("CAREBIT_ACCESS_TOKEN"),
        "Idempotency-Key" => bin2hex(random_bytes(16)),
    ],
    "json" => [
        "patient_id" => "00000000-0000-4000-8000-000000000004",
        "start_time" => "2026-01-01T09:00:00Z",
        "service_id" => "00000000-0000-4000-8000-000000000005",
        "service_variant_id" => "00000000-0000-4000-8000-000000000006",
    ],
]);
$booking = json_decode((string) $response->getBody(), true, flags: JSON_THROW_ON_ERROR);
```

<!-- /code-tabs -->

## Header contract

| Condition                                      | Response                                                    |
| ---------------------------------------------- | ----------------------------------------------------------- |
| Missing or blank `Idempotency-Key`             | `400 idempotency_key_required`                              |
| Key longer than 255 characters                 | `400 idempotency_key_too_long`                              |
| Same key + same request body                   | Original status and body, plus `Idempotency-Replayed: true` |
| Same key + different request body              | `422 idempotency_key_reused`                                |
| Same key while an earlier request is in flight | `409 idempotency_conflict` with `Retry-After: 1`            |

The `Idempotency-Replayed: true` response header indicates that the body
was replayed from storage rather than freshly computed. The status code
and body are byte-for-byte identical to the first successful response.

## Retention

Idempotency records are retained for **24 hours** and then purged. A
retry after 24 hours is treated as a fresh request.

## Retry policy

Retry `5xx` responses and network failures with exponential backoff (for
example: 1s, 2s, 4s, 8s, 16s, 30s, then every minute for 15 minutes).
Send the same `Idempotency-Key` on every retry so a duplicate request
never mutates state twice.

Do not retry `4xx` responses other than:

- `409 idempotency_conflict` - retry after the value in the `Retry-After`
  header (defaults to 1 second).
- `429 rate_limited` - retry after the value in the `Retry-After` header
  (defaults to 60 seconds).

These recipient-side retry guidelines are separate from the webhook
delivery retry schedule described in [Webhook retries](/guides/webhook-retries).
