Idempotency
X-Idempotency-Key для безопасных retry на критичные POST endpoints.
NurCore поддерживает idempotency keys на критичных POST endpoints для защиты от двойных списаний и дубликатов при retry.
Зачем нужно
Mobile/website отправляет POST /bookings/ → сеть timeout. Mobile делает
retry — без idempotency это создало бы две брони с двойным списанием.
С idempotency повторный запрос с тем же ключом → вернёт тот же ответ
без второго списания.
Endpoints с поддержкой
| Endpoint | Namespace |
|---|---|
POST /bookings/ | booking.create |
POST /bookings/{id}/initiate-payment | booking.payment.initiate |
POST /bookings/{id}/pay-with-balance | booking.payment.balance |
POST /bookings/{id}/refund | booking.refund |
Использование
Передайте X-Idempotency-Key header — любая уникальная строка (UUID
рекомендуется):
curl -X POST \
-H "X-API-Key: $SECRET_KEY" \
-H "X-Client-Id: $CLIENT_ID" \
-H "X-Client-Type: consumer_app" \
-H "X-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{...booking data...}' \
"https://api.nurcore.kg/api/v1/bookings/"Поведение
Первый запрос
NurCore выполняет создание, возвращает 201 + booking response. Параллельно кеширует ответ в Redis на 24 часа.
Повторный запрос с тем же ключом
# 5 секунд после первого:
curl -X POST -H "X-Idempotency-Key: <same-key>" ... → 201 (тот же body)Возвращает идентичный ответ — тот же booking_reference, та же
expiry_date. Бронь не дублируется.
Запрос в процессе обработки
Если клиент делает retry до того как первый запрос завершился:
1. POST /bookings/ — handler ещё работает (lock_processing в Redis)
2. POST /bookings/ same key — увидит lock → возвращает 409 Conflict
"Request with this Idempotency-Key is in progress (try again in a moment)"Клиент должен подождать 1-2 секунды и сделать retry. После завершения первого запроса все retry увидят cached response.
Истечение ключа (24h)
Через 24 часа кеш истекает. После этого запрос с тем же ключом будет обработан как новый — создаст вторую бронь. Это by design — для ограничения размера Redis.
Изоляция по client_id
Ключ масштабируется по client_id (из API key validation):
Your backend (sk_live_abc) → ключ "uuid-1" → бронь A
Astana Travel (sk_live_xyz) → ключ "uuid-1" → бронь B (отдельно!)Это исключает collision между разными партнёрами.
Best practices
Генерация ключа
// TypeScript / Node.js
import { randomUUID } from "crypto";
const idempotencyKey = randomUUID();
await fetch("/bookings/", {
method: "POST",
headers: {
"X-Idempotency-Key": idempotencyKey,
// ...
},
});# Python
import uuid
idempotency_key = str(uuid.uuid4())
response = httpx.post(
"https://api.nurcore.kg/api/v1/bookings/",
headers={"X-Idempotency-Key": idempotency_key},
json=booking_data,
)Сохранение ключа на стороне клиента
Важно: ключ должен быть тот же при retry. Если клиент генерирует новый ключ при каждой попытке — idempotency не работает.
Pattern:
- Сгенерируйте UUID один раз перед первой попыткой
- Сохраните в local storage / session
- При retry → используйте тот же ключ
- Только при новой логической операции (новая бронь) → новый ключ
// Правильно
const cartIdempotencyKey =
sessionStorage.getItem("cart-idempotency-key") ?? randomUUID();
sessionStorage.setItem("cart-idempotency-key", cartIdempotencyKey);
async function submitBooking() {
while (true) {
try {
return await fetch("/bookings/", {
headers: { "X-Idempotency-Key": cartIdempotencyKey },
...
});
} catch (e) {
if (e.status === 409) await sleep(2000); // ждём processing lock
else throw e;
}
}
}// Неправильно — каждая retry создаёт новый ключ → дублирование
async function submitBooking() {
while (true) {
await fetch("/bookings/", {
headers: { "X-Idempotency-Key": randomUUID() }, // ← BUG
});
}
}Длина ключа
- Минимум 16 символов (для статистической уникальности)
- Максимум 200 символов
- Алфавит: ASCII
UUID v4 (36 символов) — рекомендуемый формат.
Ключ ≠ авторизация
Idempotency key — это дедупликация, не авторизация. NurCore
всё равно требует X-API-Key + ownership check на брони. Передача
правильного ключа не даёт доступ к чужой брони.
Ошибки
| Code | Сценарий |
|---|---|
| 200/201 | Успешный ответ (новый или из кеша) |
| 409 | Запрос с этим ключом сейчас обрабатывается — retry через 1-2с |
FAQ
Можно ли изменить body между retries с тем же ключом?
Нельзя — это anti-pattern. NurCore не валидирует совпадение body (для производительности), но если повторный запрос имеет другой body с тем же ключом — клиент получит первый ответ (с оригинальными параметрами), что приведёт к рассинхрону на стороне клиента.
Что если я случайно использую один ключ для разных броней?
Получите ответ первой брони на запросе для второй. Поэтому всегда генерируйте новый UUID для каждой логической операции (новая бронь / новый платёж).
Поддерживается ли GET endpoints?
Нет. GET по определению idempotent (читает без изменений). Idempotency ключи нужны только для POST/PATCH, которые меняют состояние.
Что хранится в Redis?
status_code + response_body (JSON). Не размер запроса, не headers.
Только то, что нужно вернуть на retry.
Связанные документы
- Authentication — X-Client-Id используется для изоляции ключей
- Error Handling — retry policy