> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ventry.es/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> How to retry a write without duplicating anything.

Every write requires the `Idempotency-Key` header with a unique identifier that
**your system generates**:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.ventry.es/v1/tickets \
    -H "Authorization: Bearer $VENTRY_KEY" \
    -H "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7" \
    -H "Content-Type: application/json" \
    -d '{ "ticket_type": "...", "valid_days": ["..."], "attendee": { "name": "Ada", "email": "ada@example.com" } }'
  ```

  ```js Node.js theme={null}
  await fetch("https://api.ventry.es/v1/tickets", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.VENTRY_KEY}`,
      "Idempotency-Key": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      ticket_type: "...",
      valid_days: ["..."],
      attendee: { name: "Ada", email: "ada@example.com" },
    }),
  });
  ```

  ```python Python theme={null}
  requests.post(
      "https://api.ventry.es/v1/tickets",
      headers={
          "Authorization": f"Bearer {os.environ['VENTRY_KEY']}",
          "Idempotency-Key": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      },
      json={
          "ticket_type": "...",
          "valid_days": ["..."],
          "attendee": {"name": "Ada", "email": "ada@example.com"},
      },
      timeout=30,
  )
  ```

  ```php PHP theme={null}
  <?php
  $ch = curl_init("https://api.ventry.es/v1/tickets");
  curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_POST => true,
      CURLOPT_HTTPHEADER => [
          "Authorization: Bearer " . getenv("VENTRY_KEY"),
          "Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7",
          "Content-Type: application/json",
      ],
      CURLOPT_POSTFIELDS => json_encode([
          "ticket_type" => "...",
          "valid_days" => ["..."],
          "attendee" => ["name" => "Ada", "email" => "ada@example.com"],
      ]),
  ]);

  $response = json_decode(curl_exec($ch), true);
  curl_close($ch);
  ```
</CodeGroup>

## Why it is mandatory

Because the bad case is silent. You send a batch of 300 tickets, the request
times out and you do not know whether it arrived. If you retry without an
idempotency key and it had in fact arrived, you end up with 600 tickets and
nobody finds out until the headcount does not add up on the day of the event.

With the header, the retry returns the original response and executes nothing.

## Behaviour

<CardGroup cols={2}>
  <Card title="Same key, same body" icon="check">
    Returns the stored response without executing again. The response carries the
    `Idempotent-Replayed: true` header.
  </Card>

  <Card title="Same key, different body" icon="triangle-exclamation">
    `422 idempotency_key_reused`. It is a safety net: it means you reused a key by
    mistake.
  </Card>

  <Card title="Key in flight" icon="clock">
    `409 idempotency_in_flight` if the operation is being processed at that very
    moment. Wait a second and retry.
  </Card>

  <Card title="Different keys" icon="copy">
    Two independent operations. Sending the same batch with two different keys
    **does** create everything twice.
  </Card>
</CardGroup>

## How to generate the key

A UUID v4 will do. What matters is that it is **stable across retries** of the
same logical operation and different between different operations.

<CodeGroup>
  ```js Node.js theme={null}
  // Good: the key is generated once and reused across retries.
  const idempotencyKey = crypto.randomUUID();
  await withRetries(() => createTickets(batch, idempotencyKey));

  // Bad: every retry generates a new key and duplicates the batch.
  await withRetries(() => createTickets(batch, crypto.randomUUID()));
  ```

  ```python Python theme={null}
  import uuid

  # Good: the key is generated once and reused across retries.
  idempotency_key = str(uuid.uuid4())
  with_retries(lambda: create_tickets(batch, idempotency_key))

  # Bad: every retry generates a new key and duplicates the batch.
  with_retries(lambda: create_tickets(batch, str(uuid.uuid4())))
  ```

  ```php PHP theme={null}
  <?php
  // Good: the key is generated once and reused across retries.
  $idempotencyKey = bin2hex(random_bytes(16));
  withRetries(fn () => createTickets($batch, $idempotencyKey));

  // Bad: every retry generates a new key and duplicates the batch.
  withRetries(fn () => createTickets($batch, bin2hex(random_bytes(16))));
  ```
</CodeGroup>

If your operations already have an identifier of their own — an order number, say
— use it. It is more robust than an in-memory UUID, because it survives your own
process crashing:

<CodeGroup>
  ```js Node.js theme={null}
  const idempotencyKey = `order-${order.id}-tickets`;
  ```

  ```python Python theme={null}
  idempotency_key = f"order-{order.id}-tickets"
  ```

  ```php PHP theme={null}
  $idempotencyKey = "order-{$order->id}-tickets";
  ```
</CodeGroup>

## Scope

The key is yours: two different integrators can use the same value without
colliding, and the same key on two different endpoints are two different
operations.

Completed operations are remembered for **7 days**. After that, the same key is
treated as a new operation.

## The accreditation case

`POST /tickets/{code}/check-in` uses the `Idempotency-Key` as the scan
identifier. That makes it especially safe to retry: if the network dropped right
after the ticket was redeemed, the retry returns **the same wristband** instead
of issuing a second one.

Generate one key per physical scan and keep it for the duration of the retries:

<CodeGroup>
  ```js Node.js theme={null}
  const scanId = crypto.randomUUID();  // once, when the QR is read

  await checkIn(ticketCode, wristbandNfc, scanId);  // safe to retry
  ```

  ```python Python theme={null}
  scan_id = str(uuid.uuid4())  # once, when the QR is read

  check_in(ticket_code, wristband_nfc, scan_id)  # safe to retry
  ```

  ```php PHP theme={null}
  $scanId = bin2hex(random_bytes(16));  // once, when the QR is read

  checkIn($ticketCode, $wristbandNfc, $scanId);  // safe to retry
  ```
</CodeGroup>
