Идемпотентность

Idempotency-Key — безопасные повторы POST-запросов без дубликатов

Идемпотентность гарантирует, что повтор одного и того же запроса не создаст второй платёж. Pay Bot хранит результат предыдущего вызова 24 часа и возвращает его на повторный запрос с тем же ключом.

Зачем нужно

Сеть ненадёжна. Ваш POST может «потеряться» — но Pay Bot уже обработал его и не успел вернуть 200. Повтор без идемпотентности создаст второй QR / счёт / возврат. Это особенно опасно для возвратов: можно вернуть клиенту вдвое больше денег.

С Idempotency-Key:

  • При первом запросе Pay Bot обрабатывает и кеширует ответ
  • При повторе с тем же ключом возвращает кешированный ответ за миллисекунды
  • Гарантия: ровно один платёж независимо от количества попыток

Где обязателен

Обязателен на этих эндпоинтах:

  • POST /v2/qr
  • POST /v2/invoices
  • POST /v2/refunds

Без заголовка Idempotency-Key придёт 400 idempotency_key_required.

Опционален (но рекомендуется) на POST /v2/payment-links — Pay Bot сам генерирует ключ из тела запроса, но явный Idempotency-Key надёжнее.

Не нужен на GET, DELETE, и PATCH-эндпоинтах — они и так идемпотентны.

Как использовать

bash
Скачать
curl -X POST https://payapi.aibot.kz/v2/qr \
  -H "X-API-Key: kp_live_xxx" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"amount": 5000}'
python
Скачать
import uuid, requests

key = str(uuid.uuid4())  # сгенерировать один раз
for attempt in range(3):
    try:
        resp = requests.post(
            "https://payapi.aibot.kz/v2/qr",
            headers={
                "X-API-Key": "kp_live_xxx",
                "Idempotency-Key": key,  # тот же ключ на всех retry
            },
            json={"amount": 5000},
            timeout=10,
        )
        return resp.json()
    except requests.Timeout:
        continue
javascript
Скачать
const key = crypto.randomUUID();

async function createWithRetry() {
  for (let i = 0; i < 3; i++) {
    try {
      const res = await fetch("https://payapi.aibot.kz/v2/qr", {
        method: "POST",
        headers: {
          "X-API-Key": "kp_live_xxx",
          "Idempotency-Key": key,  // тот же ключ
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ amount: 5000 }),
      });
      return await res.json();
    } catch (e) {
      if (i === 2) throw e;
    }
  }
}
php
Скачать
$key = bin2hex(random_bytes(16));

$ch = curl_init("https://payapi.aibot.kz/v2/qr");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "X-API-Key: kp_live_xxx",
        "Idempotency-Key: $key",  // тот же на retry
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => json_encode(["amount" => 5000]),
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);

Правила

  • Длина ключа: 1–255 символов
  • Допустимые символы: [A-Za-z0-9_-]
  • TTL кеша: 24 часа
  • Уникальность: в пределах одного аккаунта
  • Тело запроса при повторе должно совпадать — иначе 409 idempotency_conflict

Что в ответе

При повторе Pay Bot добавляет заголовок:

code
X-Idempotent-Replay: true

Тело и статус-код идентичны оригинальному ответу. Логируйте этот заголовок — он помогает понять, что произошёл retry.

Conflict при разных телах

Если вы переиспользовали ключ с другим телом запроса, Pay Bot вернёт:

json
Скачать
{
  "error": {
    "type": "validation_error",
    "code": "idempotency_conflict",
    "message": "Idempotency-Key уже использован с другим телом запроса",
    "request_id": "req_..."
  }
}

Используйте новый ключ или повторите с оригинальным телом.

Лучшие практики

  • Генерируйте ключ один раз перед циклом retry, не на каждой попытке
  • Сохраняйте ключ в БД вместе с заказом — это даёт идемпотентность даже при перезапуске сервера
  • Используйте бизнес-идентификаторы: Idempotency-Key: refund_{order_id}_{attempt} или qr_{cart_id}
  • Не реюзайте через 24 часа — кеш истёк, повтор создаст новую операцию

Пример: устойчивое создание возврата

python
Скачать
import uuid, requests

def create_refund(order_id, amount):
    # Ключ зависит от order_id — гарантия одного возврата на заказ
    idem = f"refund_{order_id}"

    resp = requests.post(
        "https://payapi.aibot.kz/v2/refunds",
        headers={
            "X-API-Key": "kp_live_xxx",
            "Idempotency-Key": idem,
        },
        json={"operation_id": find_op_id(order_id), "amount": amount},
        timeout=10,
    )

    if resp.headers.get("X-Idempotent-Replay") == "true":
        logger.info(f"refund replay for order {order_id}")

    return resp.json()

Даже если функция вызывается N раз для одного заказа — возврат произойдёт один раз.

Что дальше