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 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"}'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();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: trueheader. - 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 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}}'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();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.