Ідемпотентність
Через мережевий збій ви можете не знати, чи дійшов запит. Без захисту «безпечний» повтор може перевірити ту саму адресу двічі. Ключ ідемпотентності робить повтор нешкідливим: API впізнає його й відповідає результатом, який уже записав.
Заголовок
Надсилайте Idempotency-Key: <значення> у кожному запиті, що щось створює:
POST /v1/checksіPOST /v1/checks/batchPOST /v1/webhooks,POST /v1/webhooks/{webhook_id}/rotateіDELETE /v1/webhooks/{webhook_id}POST /v1/telegram/link-codesіDELETE /v1/telegram/links/{link_id}
Заголовок необов'язковий. Значення — від 1 до 255 друкованих символів ASCII без пробілів; усе інше відхиляється з 400 VALIDATION_ERROR і parameter: "idempotency-key". Використовуйте власний ідентифікатор операції (номер замовлення, ідентифікатор виплати), а не випадкове значення на кожну спробу: суть у тому, що кожна спроба однієї операції надсилає той самий ключ.
Ключ діє в межах вашого облікового запису, середовища ключа (live чи sandbox) та ендпоінта, тож те саме значення на двох ендпоінтах або під ключами sandbox і live не конфліктує. Ключі пам'ятаються 24 години; після цього значення можна використати знову.
Що відбувається, коли ключ використано повторно
| Ви надсилаєте | API відповідає |
|---|---|
| Ключ уперше | Виконує запит і записує його результат під цим ключем. |
| Той самий ключ і те саме тіло, коли перший запит уже завершився | Відтворює записаний результат: той самий код статусу й той самий ресурс (для перевірки — той самий check_id). Нічого не перевіряється й не тарифікується вдруге. |
| Той самий ключ і інше тіло | 422 IDEMPOTENCY_KEY_CONFLICT. Нічого не виконується. |
| Той самий ключ, поки перший запит ще виконується | 409 IDEMPOTENCY_REQUEST_IN_FLIGHT. Зачекайте мить і надішліть знову. |
| Той самий ключ після того, як оренду першого запиту перехопив одночасний повтор | 409 IDEMPOTENCY_LEASE_LOST для запиту, який її втратив. Надішліть той самий запит ще раз, щоб прочитати записаний результат. |
Деталі, важливі на практиці:
- «Те саме тіло» означає ті самі байти. Тіло порівнюється так, як надіслано, а не після розбору, тож повтор, що серіалізує JSON з іншим порядком ключів чи пробілами, вважається іншим тілом. Зберігайте серіалізований запит і надсилайте його без змін.
- Записуються лише успішні результати. Запит, що завершився
4xxабо5xx, не прив'язується до свого ключа, тож повтор виконується знову, і саме цього ви хочете після тимчасового збою. Якщо помилка була4xx, спершу виправте запит. - Збійний запит не блокує ключ. Якщо перша спроба обривається без відповіді, повторний запит перехоплює ключ через 30 секунд.
- Секрет вебхука показується один раз. Відтворення
POST /v1/webhooksчи ротації повертає вебхук безsecret, бо секрет ніколи не зберігається у читабельному вигляді. Якщо ви втратили відповідь, виконайте ротацію з новим ключем. - Елементи пакета отримують похідні ключі. Ключ кожного елемента пакета утворюється з ключа пакета та його позиції, тож повторне відтворення пакета не може перевірити елемент двічі.
Приклад
Надсилайте той самий запит із тим самим ключем і тим самим тілом, доки не отримаєте відповідь. Тіло тут — тестова адреса sandbox, яка завжди відповідає pending, тож запускайте приклад ключем sandbox.
- 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"}'
Запустіть двічі: друга відповідь міститиме той самий check_id, що й перша.
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 {
// мережевий збій: запит міг дійти або ні, тож повторюємо з тим самим ключем
}
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 # запит міг дійти або ні, тож повторюємо з тим самим ключем
time.sleep(2**attempt * 0.25)
raise RuntimeError("check not confirmed after 5 attempts")
print(screen("deposit-2291"))
Про реакцію на інші збої див. Помилки.