Webhooks

Отримуйте та перевіряйте події термінального статусу замовлень

View as Markdown

Коли замовлення, розміщене через API, досягає термінального статусу, Voodoo Center надсилає POST із підписаною JSON-подією на налаштовану вами webhook-URL. Це рекомендований спосіб дізнатися результат замовлення — він базується на push, тож вам не потрібно виконувати опитування.

Коли спрацьовують webhook

Webhook надсилається, коли виконуються всі такі умови:

  • Замовлення досягає термінального статусу: completed, failed або partial.
  • Замовлення було розміщене через API (source: "api"). Замовлення, створені в дашборді, не запускають webhook.
  • Для вашого акаунта налаштовано webhook-URL.

Тіло запиту

Тіло запиту — це компактний JSON із відсортованими ключами з такими полями:

ПолеТипОпис
order_idрядок (uuid)Id замовлення (збігається з id з API).
merchant_order_idрядокВаш ідентифікатор, якщо ви його надали.
statusрядокТермінальний статус: completed, failed або partial.
item_idціле число | nullЧисловий id товару.
item_nameрядокЧитабельна назва товару.
quantityчислоЗамовлена кількість.
total_priceчислоСписана сума в основних грошових одиницях.
delivered_quantityціле числоФактично доставлені одиниці.
refund_amountчислоСума, повернута за недоставлені одиниці.
fieldsобʼєктНадіслані поля вводу (значення вибору як відображувані назви).
codesstring[]Рядки доставлених ключів/кодів (товари типу key).
errorрядокМашинний код помилки в разі збою (наприклад, OUT_OF_STOCK).
error_messageрядокЧитабельне повідомлення про помилку в разі збою.
completed_atрядок (date-time) | nullКоли замовлення було виконано.
created_atрядок (date-time)Коли замовлення було створено.
Виконане замовлення з ключами
1{
2 "order_id": "0190f8a1-6b2c-7e33-9a10-4c1d2e3f5a6b",
3 "merchant_order_id": "po-10231",
4 "status": "completed",
5 "item_id": 4090,
6 "item_name": "Sample Gift Card 10 USD",
7 "quantity": 2,
8 "total_price": 18.00,
9 "delivered_quantity": 2,
10 "refund_amount": 0,
11 "fields": {},
12 "codes": ["ABCD-1234-EFGH-5678", "IJKL-9012-MNOP-3456"],
13 "error": "",
14 "error_message": "",
15 "completed_at": "2026-07-05T12:00:07Z",
16 "created_at": "2026-07-05T12:00:00Z"
17}
Часткова доставка з поверненням
1{
2 "order_id": "0190f8a2-7c3d-7f44-ab21-5d2e3f4a6b7c",
3 "merchant_order_id": "po-10240",
4 "status": "partial",
5 "item_id": 4090,
6 "item_name": "Sample Gift Card 10 USD",
7 "quantity": 3,
8 "total_price": 27.00,
9 "delivered_quantity": 1,
10 "refund_amount": 18.00,
11 "fields": {},
12 "codes": ["ABCD-1234-EFGH-5678"],
13 "error": "",
14 "error_message": "",
15 "completed_at": null,
16 "created_at": "2026-07-05T12:10:00Z"
17}
Невдале замовлення (повністю повернуто)
1{
2 "order_id": "0190f8a3-8d4e-7055-bc32-6e3f4a5b7c8d",
3 "merchant_order_id": "po-10250",
4 "status": "failed",
5 "item_id": 4211,
6 "item_name": "Sample Game Top-up",
7 "quantity": 10,
8 "total_price": 12.50,
9 "delivered_quantity": 0,
10 "refund_amount": 12.50,
11 "fields": { "player_id": "123456789", "server": "Europe (EU-West)" },
12 "codes": [],
13 "error": "OUT_OF_STOCK",
14 "error_message": "Out of stock",
15 "completed_at": null,
16 "created_at": "2026-07-05T12:20:00Z"
17}

Перевірка підпису

Кожен webhook несе заголовок:

X-Webhook-Signature: sha256=<hex>

де <hex> — це HMAC-SHA256(webhook_secret, raw_request_body_bytes).

Перевіряйте щодо сирих отриманих байтів, точно так, як їх доставлено — ніколи щодо словника, який ви пересеріалізували. Підписане тіло — це компактний JSON із відсортованими ключами, тож будь-яке перекодування (інший порядок ключів, пробіли чи форматування чисел) дасть інший підпис і не пройде перевірку.

Обчисліть той самий HMAC зі своїм секретом для підпису та порівняйте його із заголовком, використовуючи порівняння сталого часу (constant-time).

Приймач і верифікатор на FastAPI

Цей приймач перевіряє сирі байти перед парсингом, використовує порівняння сталого часу та швидко повертає 200:

webhook_receiver.py
1import hmac, hashlib
2from fastapi import FastAPI, Request, Header, HTTPException
3
4app = FastAPI()
5WEBHOOK_SECRET = "whsec_your_secret_from_the_dashboard"
6
7@app.post("/webhooks/voodoo-center")
8async def voodoo_webhook(request: Request, x_webhook_signature: str = Header(default="")):
9 raw = await request.body() # перевірте СИРІ байти ПЕРЕД парсингом JSON
10 expected = "sha256=" + hmac.new(WEBHOOK_SECRET.encode(), raw, hashlib.sha256).hexdigest()
11 if not hmac.compare_digest(x_webhook_signature, expected):
12 raise HTTPException(status_code=401, detail="invalid signature")
13 event = await request.json()
14 # Ідемпотентність: подію можна отримати більше одного разу — дедуплікуйте за order_id.
15 # process(event["status"], event.get("codes"), event.get("refund_amount"), ...)
16 return {"ok": True} # швидко повертайте 200; важку роботу робіть у фоні

Підтвердження, повторні спроби та таймаути

  • Підтверджуйте HTTP 200, щойно ви зберегли подію. Будь-яка відповідь, що не є 200 (або таймаут), вважається невдалою доставкою.
  • Невдалі доставки повторюються до 3 спроб із інтервалом 60 секунд.
  • Кожна спроба має таймаут 30 секунд.
  • Переспрямування не виконуються, а ваша URL має бути публічно маршрутизованою — захист від підробки запитів на боці сервера (SSRF) блокує внутрішні/приватні адреси.

Оскільки одну подію може бути доставлено більше одного разу (наприклад, повторна спроба після того, як ваш 200 надійшов із затримкою), зробіть свій обробник ідемпотентним: дедуплікуйте за order_id і сприймайте повторну доставку вже обробленої події як no-op.

Виконуйте важку роботу (виконання власного подальшого замовлення, надсилання листа тощо) у фоні та швидко повертайте 200 — повільний обробник ризикує таймаутом у 30 секунд і зайвою повторною спробою.

Налаштування URL і ротація секрету

Обидва налаштування webhook розміщені на сторінці API в дашборді (вони доступні лише в дашборді, а не як ендпоінти API):

  • Webhook URL — куди надсилаються (POST) події. Має бути публічно доступною через HTTPS.
  • Секрет для підпису — використовується для обчислення X-Webhook-Signature. Його формат — whsec_..., і він показується один раз, під час створення/ротації. Виконайте ротацію, якщо він міг витекти, і оновіть WEBHOOK_SECRET у своєму приймачі, щоб він відповідав.