Идемпотентность гарантирует, что повторный запрос не создаст второй платёж или возврат. Передавайте заголовок Idempotency-Key (обычно UUID v4) на мутирующих POST-запросах — если сеть оборвалась и вы отправили запрос заново с тем же ключом, PayBot вернёт результат первой операции, а не проведёт её дважды.
Ключ обязателен на POST /v2/qr, POST /v2/invoices, POST /v2/invoices/{id}/cancel, POST /v2/refunds и на всех мутирующих /v1/*. Для POST /v2/payment-links он необязателен — бэкенд подставит собственный, но свой ключ защитит от двойного клика.
Как передать ключ идемпотентности
Добавьте заголовок Idempotency-Key со случайной строкой. На практике — UUID версии 4.
curl -X POST https://api.paybot.kz/v2/qr \
-H "X-API-Key: kp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Idempotency-Key: 3f9a1c8e-2b7d-4056-a1c2-e3f4b5d6c7e8" \
-H "Content-Type: application/json" \
-d '{"amount": 5000, "comment": "Заказ №1024"}'import uuid
import requests
def create_qr(api_key: str, amount: int, comment: str) -> dict:
idempotency_key = str(uuid.uuid4()) # один ключ на намерение пользователя
response = requests.post(
"https://api.paybot.kz/v2/qr",
headers={
"X-API-Key": api_key,
"Idempotency-Key": idempotency_key,
},
json={"amount": amount, "comment": comment},
)
return response.json()Как правильно формировать ключ
Генерируйте один ключ на одно намерение пользователя — на нажатие кнопки «Оплатить», а не на каждую HTTP-попытку. Смысл в этом: если запрос упал по таймауту и вы повторяете ту же операцию, повтор должен идти с тем же ключом, иначе PayBot сочтёт его новой операцией и создаст второй платёж.
- •UUID v4 — надёжный дефолт:
str(uuid.uuid4())в Python,crypto.randomUUID()в JS. - •Ключ можно и осмысленный — например, детерминированный из ID заказа, чтобы повтор оплаты того же заказа гарантированно совпал.
- •Не переиспользуйте один ключ для разных по смыслу операций — совпадение ключей означает «это тот же запрос».
Что происходит при повторе
- •Повтор с тем же ключом на
/v1/*вернёт тот же ответ и добавит заголовокX-Idempotent-Replay: true— можно честно показать пользователю «повтор, деньги не списаны». - •Параллельный запрос с тем же ключом, пока первый ещё выполняется, вернёт
409 idempotency_conflict— не ретрайте агрессивно, дождитесь ответа первого. - •Отсутствие обязательного ключа даст
400 idempotency_key_required(param: Idempotency-Key) — это баг фронтенда, а не ошибка пользователя, не показывайте её в интерфейсе.
Отдельная защита есть у оплаты подписки: POST /v1/subscription/pay держит Redis-лок на пару (клиент, тариф) 30 секунд, поэтому двойной клик вернёт 429 payment_in_progress — показывайте это как нейтральный тост «подождите 30 секунд», а не как ошибку.
Окно хранения и режим test
Ключ действует в пределах окна дедупликации на бэке: в течение него повтор возвращает сохранённый результат первой операции. По истечении окна тот же ключ снова считается новой операцией — не рассчитывайте, что старый ключ защитит спустя сутки. Для POST /v2/payment-links, где ключ необязателен, бэкенд сам генерирует значение вида pl_auto_.
В тестовом режиме (ключ kp_test_...) идемпотентность работает так же — отлаживайте повторы на тестовых платежах, прежде чем включать боевой ключ.
FAQ
Что будет, если не передать Idempotency-Key?
На обязательных endpoint'ах (/v2/qr, /v2/invoices, /v2/refunds, cancel, все /v1/*) вернётся 400 idempotency_key_required. Исключение — план enterprise на /v1/*, где ключ генерируется автоматически. Для POST /v2/payment-links ключ необязателен и подставляется сам.
Какой ключ использовать — случайный или из ID заказа?
Оба варианта рабочие. Случайный UUID проще и безопасен, если вы держите его на время намерения пользователя. Детерминированный ключ из ID заказа удобнее, когда нужно, чтобы любая повторная попытка оплатить конкретный заказ гарантированно схлопнулась в одну операцию.
Идемпотентность защищает от двойного клика?
Да, если оба клика уходят с одним ключом. Сгенерируйте ключ в момент открытия формы (одно намерение) и шлите его на всех повторах — второй клик тогда либо вернёт результат первого, либо получит 409 idempotency_conflict.
Чем отличается replay от conflict?
X-Idempotent-Replay: true — первая операция уже завершилась, вам вернули её готовый результат. 409 idempotency_conflict — первая операция ещё выполняется прямо сейчас, повтор пришёл слишком рано; подождите и не дублируйте запрос.
Смежные разделы
- •QR и счета — где ключ идемпотентности обязателен.
- •Коды ошибок — как обрабатывать
idempotency_key_requiredиidempotency_conflict.