Idempotency
A network failure leaves you not knowing whether a request arrived. Without protection, the safe-looking retry could screen the same address twice. An idempotency key makes the retry harmless: the API recognises it and answers with the result it already recorded.
The header
Send Idempotency-Key: <value> on any request that creates something:
POST /v1/checksandPOST /v1/checks/batchPOST /v1/webhooks,POST /v1/webhooks/{webhook_id}/rotateandDELETE /v1/webhooks/{webhook_id}POST /v1/telegram/link-codesandDELETE /v1/telegram/links/{link_id}
The header is optional. The value is 1 to 255 printable ASCII characters with no spaces; anything else is rejected with 400 VALIDATION_ERROR and parameter: "idempotency-key". Use your own operation id (an order number, a payout id), not a random value generated per attempt: the whole point is that every attempt of one operation sends the same key.
A key is scoped to your account, the key environment (live or sandbox) and the endpoint, so the same value used on two endpoints, or under a sandbox and a live key, does not collide. Keys are remembered for 24 hours; after that the value can be reused.
What happens when a key is reused
| You send | The API answers |
|---|---|
| The key for the first time | Runs the request and records its result under the key. |
| The same key and the same body, after the first request finished | Replays the recorded result: the same status code and the same resource (for a check, the same check_id). Nothing is screened and nothing is charged again. |
| The same key and a different body | 422 IDEMPOTENCY_KEY_CONFLICT. Nothing runs. |
| The same key while the first request is still running | 409 IDEMPOTENCY_REQUEST_IN_FLIGHT. Wait a moment and send it again. |
| The same key after the first request's lease was taken over by a concurrent retry | 409 IDEMPOTENCY_LEASE_LOST on the request that lost it. Send the same request again to read the recorded result. |
Details that matter in practice:
- "Same body" means the same bytes. The body is compared as sent, not after parsing, so a retry that re-serialises the JSON with a different key order or spacing counts as a different body. Store the serialised request and resend it unchanged.
- Only successful results are recorded. A request that ends in a
4xxor5xxis not pinned to its key, so a retry runs again, which is what you want after a transient failure. Fix the request first if the error was a4xx. - A crashed request does not block the key. If the first attempt dies without answering, a retry takes the key over after 30 seconds.
- A webhook secret is shown once. Replaying
POST /v1/webhooksor a rotation returns the webhook without itssecret, because the secret is never stored in readable form. If you lost the response, rotate with a new key. - Batch items get derived keys. Each item of a batch is keyed from the batch key and its position, so replaying a batch cannot screen an item twice.
Example
Send the same request, with the same key and the same body, until you get an answer. The body here is the sandbox test address, which always answers pending, so run it under a sandbox key.
- curl
- Node.js
- Python
curl -s -X POST https://api.leeri.ai/v1/checks \
-H "Authorization: Bearer $LEERI_API_KEY" \
-H "Idempotency-Key: deposit-2291-attempt-group" \
-H "Content-Type: application/json" \
-d '{"subject":"address","asset":"BTC","address":"1PENDNGCHECKADDRESSFRSANDBXTEST99"}'
Run it twice: the second answer carries the same check_id as the first.
const body = JSON.stringify({ subject: 'address', asset: 'BTC', address: '1PENDNGCHECKADDRESSFRSANDBXTEST99' });
async function screen(operationId) {
for (let attempt = 1; attempt <= 5; attempt += 1) {
try {
const response = await fetch('https://api.leeri.ai/v1/checks', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LEERI_API_KEY}`,
'Idempotency-Key': operationId,
'Content-Type': 'application/json',
},
body,
});
const retryable = response.status === 409 || response.status >= 500;
if (!retryable) return await response.json();
} catch {
// network failure: the request may or may not have arrived, so retry with the same key
}
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 250));
}
throw new Error('check not confirmed after 5 attempts');
}
console.log(await screen('deposit-2291'));
import json
import os
import time
import requests
body = json.dumps({"subject": "address", "asset": "BTC", "address": "1PENDNGCHECKADDRESSFRSANDBXTEST99"})
def screen(operation_id):
for attempt in range(1, 6):
try:
response = requests.post(
"https://api.leeri.ai/v1/checks",
headers={
"Authorization": f"Bearer {os.environ['LEERI_API_KEY']}",
"Idempotency-Key": operation_id,
"Content-Type": "application/json",
},
data=body,
timeout=30,
)
if response.status_code != 409 and response.status_code < 500:
return response.json()
except requests.RequestException:
pass # the request may or may not have arrived, so retry with the same key
time.sleep(2**attempt * 0.25)
raise RuntimeError("check not confirmed after 5 attempts")
print(screen("deposit-2291"))
See Errors for how to react to the other failures.