# Idempotency

https://gorally.games/en/developers/idempotency

A write can fail halfway: your connection drops. Your software then does not know whether Rally received the request. An `Idempotency-Key` lets you send it again with no risk of a duplicate.

## Which requests

Send an `Idempotency-Key` on every `POST`. It is optional, but without it a second send creates a duplicate.

- Use a unique value per request, for example a UUID.
- It is never empty and is 255 characters at most. Otherwise the API answers `INVALID_INPUT`.
- Rally keeps it for 24 hours.

**curl**

```bash
curl https://gorally.games/v1/leagues \
  -X POST \
  -H "Authorization: Bearer $RALLY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6a1f0c3e-creer-ligue-mardi" \
  -d '{"name":"Ligue du mardi","sport":"CURLING","timeZone":"America/Toronto"}'
```

**JavaScript**

```js
const response = await fetch('https://gorally.games/v1/leagues', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.RALLY_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': '6a1f0c3e-creer-ligue-mardi',
  },
  body: JSON.stringify({
    "name": "Ligue du mardi",
    "sport": "CURLING",
    "timeZone": "America/Toronto"
  }),
});
const data = await response.json();
```

**Python**

```python
import os

import requests

response = requests.post(
    "https://gorally.games/v1/leagues",
    headers={
        "Authorization": f"Bearer {os.environ['RALLY_API_KEY']}",
        "Idempotency-Key": "6a1f0c3e-creer-ligue-mardi",
    },
    json={
        "name": "Ligue du mardi",
        "sport": "CURLING",
        "timeZone": "America/Toronto",
    },
    timeout=10,
)
data = response.json()
```

## What the API does

Rally compares the key and the request body, byte for byte.

- First time: the request runs, and its response is stored.
- Same key, same body: Rally replays the original response and writes nothing again. The replay carries the `Idempotent-Replayed: true` header.
- Same key, different body: the API refuses.

A refusal returned during execution is replayed like a success: missing rights, item not found, conflict. A `5xx` is never stored: it means "retry".

A body rejected while being read does not reserve the key: malformed JSON, invalid or missing field. Fix the request and send it again with the same key: it runs, without `Idempotent-Replayed`.

The replay carries the `request_id` of your new request, not the original one.

## The three errors

They all answer `409`.

| Code | Cause | What to do |
|---|---|---|
| `IDEMPOTENCY_KEY_REUSED` | This key was already used with a different body. | Use a new key for this new request. |
| `IDEMPOTENT_REQUEST_IN_FLIGHT` | A request with this key is still running. | Retry in a moment, with the same key. |
| `IDEMPOTENCY_OUTCOME_UNKNOWN` | A request with this key was interrupted with no known outcome. | Read the item to see whether it exists, then resume with a new key. |

For `IDEMPOTENCY_OUTCOME_UNKNOWN`, Rally does not replay: a connection lost right after the write looks like one lost right before. Replaying could create the duplicate the key exists to prevent.

## Not on PATCH

Never send an `Idempotency-Key` on a `PATCH`. The API ignores it.

`PATCH /matches/{matchId}` protects itself another way, with a revision lock. You send `expectedRevision`, the revision you read. If someone wrote in the meantime, the API answers `CONFLICT`.

**curl**

```bash
curl https://gorally.games/v1/matches/f4e3d2c1-b0a9-4f8e-8d7c-6b5a4f3e2d1c \
  -X PATCH \
  -H "Authorization: Bearer $RALLY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"expectedRevision":3,"event":"COMPLETER","score":{"a":7,"b":5,"unitsPlayed":8}}'
```

**JavaScript**

```js
const response = await fetch('https://gorally.games/v1/matches/f4e3d2c1-b0a9-4f8e-8d7c-6b5a4f3e2d1c', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.RALLY_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "expectedRevision": 3,
    "event": "COMPLETER",
    "score": {
      "a": 7,
      "b": 5,
      "unitsPlayed": 8
    }
  }),
});
const data = await response.json();
```

**Python**

```python
import os

import requests

response = requests.patch(
    "https://gorally.games/v1/matches/f4e3d2c1-b0a9-4f8e-8d7c-6b5a4f3e2d1c",
    headers={"Authorization": f"Bearer {os.environ['RALLY_API_KEY']}"},
    json={
        "expectedRevision": 3,
        "event": "COMPLETER",
        "score": {
            "a": 7,
            "b": 5,
            "unitsPlayed": 8,
        },
    },
    timeout=10,
)
data = response.json()
```

After a `CONFLICT`, read the match again and decide. Do not send it blindly again: you would overwrite the other program’s work.

## What next

- Read the "Pagination" guide to walk through lists.
- Keep the "Errors" guide at hand for the other codes.
