NurCore API
Getting Started

Idempotency

X-Idempotency-Key для безопасных retry на критичные POST endpoints.

NurCore поддерживает idempotency keys на критичных POST endpoints для защиты от двойных списаний и дубликатов при retry.

Зачем нужно

Mobile/website отправляет POST /bookings/ → сеть timeout. Mobile делает retry — без idempotency это создало бы две брони с двойным списанием. С idempotency повторный запрос с тем же ключом → вернёт тот же ответ без второго списания.

Endpoints с поддержкой

EndpointNamespace
POST /bookings/booking.create
POST /bookings/{id}/initiate-paymentbooking.payment.initiate
POST /bookings/{id}/pay-with-balancebooking.payment.balance
POST /bookings/{id}/refundbooking.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:

  1. Сгенерируйте UUID один раз перед первой попыткой
  2. Сохраните в local storage / session
  3. При retry → используйте тот же ключ
  4. Только при новой логической операции (новая бронь) → новый ключ
// Правильно
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.

Связанные документы

On this page