# Idempotency

How to retry a request after a timeout or a dropped connection without selling one ticket twice.

Connections drop. A request may arrive and run while its response gets lost. To keep a retry from creating a second order, send the `Idempotency-Key` header:

```http
Idempotency-Key: order-1042
```

## Rules {#rules}

- The key counts in `POST` and `DELETE` requests. `GET`, `PATCH` and `PUT` don't need it.
- The key is any string up to 255 characters. Your own order number works well.
- A key lives 24 hours from the first request. Keys of different organizations and modes never overlap: a test request never gets a live response.
- A retry with the same key, method, path and body gets the stored response: the same status and the same body byte for byte, with the `Idempotent-Replayed: true` header.
- We compare the body without whitespace between JSON elements, but field order and number spelling matter. `{"a":1,"b":2}` and `{"b":2,"a":1}` are different requests.
- The same key with a different body or path gets `409 idempotency_conflict`.

## Where the key is required {#required}

[`POST /v1/events/{id}/issue`](https://budu.events/en/docs/issue/#issue) answers `400 invalid_request` without a key. Orders, reservations and events don't require one, but without it a retry creates a new object. Send the key with every request that creates something.

## What a retry returns {#outcomes}

| First request | Retry with the same key |
|---|---|
| answered `2xx` | the same response |
| answered `4xx`, such as `409 sold_out` | the same refusal, even if tickets later went back on sale. A new attempt needs a new key |
| still running | `409 idempotency_conflict`: retry in a few seconds |
| failed with `5xx` before we started creating the object | runs again |
| failed with `5xx` while creating an order, an event or an issue | `409 idempotency_conflict` with `details.idempotency_state: "unknown"`, see below |
| a reservation confirm answered `202` | runs again and shows the order's current state |

Cancelling a reservation, cancelling a ticket, reissuing and publishing are safe by themselves: after a failure on our side a retry simply runs again.

## Unknown outcome {#unknown}

If creating an order broke off on our side, the order may already exist. A retry with the same key answers like this:

```json Response 409
{
  "error": {
    "code": "idempotency_conflict",
    "message": "Результат запроса с этим Idempotency-Key неизвестен: он мог выполниться. Проверьте состояние, прежде чем повторять запрос с новым ключом",
    "details": {
      "idempotency_state": "unknown",
      "order_id": "0193e0c5-1f20-7c3e-b6a1-9d4e2f7a8b10"
    }
  }
}
```

Read the order from `details.order_id` with [`GET /v1/orders/{id}`](https://budu.events/en/docs/orders/#get). An order in `paid` or `issued` status is already sold, so you don't need a second one. Create a new order with a new key only if there is no order or it is `cancelled`.

## Where the key has no effect {#ignored}

- `POST /v1/devices/invites`: every call creates a new invite.
- `POST /v1/events/{id}/seats`: a seat upload replaces every seat of the draft, so repeating the same file gives the same result.
- `POST /v1/events/{id}/imports`: the same guest list for the same event returns the earlier import with `repeated_file: true` and issues no tickets the second time.
- `PUT /v1/events/{id}/cover`: a retry sets the same cover.
