PDF
Розділ: Вебхуки

Вебхуки

Вихідні вебхуки ApexSFA про події заявок: формат, підпис HMAC-SHA256, повтори, ідемпотентність, перевірка на Python і JavaScript.

ApexSFA надсилає HTTP POST на ваш URL, коли змінюється стан заявки. Подія фіксується разом зі зміною заявки й доставляється асинхронно, з повторами.

Вебхуки налаштовує команда ApexSFA: передайте нам URL-приймач (HTTPS) і перелік подій — ми видамо секрет підпису.

6.1. Події

ПодіяКоли надсилається
order.approvedзаявку підтверджено
order.in_deliveryзаявка перейшла в «у доставці»
order.deliveredзаявку доставлено
order.completedзаявку завершено
order.rejectedзаявку відхилено
order.cancelledзаявку скасовано
order.status_changedстатус змінено вручну на порталі (payload.manual = true, payload.reason)
webhook.testтестовий пінг, який ми надсилаємо під час налаштування

Інші типи подій (оплати, візити, точки, обладнання) — у розробці; про появу повідомимо.

6.2. Формат запиту

HTTP
POST <ваш url>
Content-Type: application/json
X-Webhook-Signature: t=1793065552,v1=61dc7efddcddec49d71d8a8f719092811654a1916501f2b6dedbd5a672ed6ce1
X-Webhook-Event: order.approved
X-Webhook-Tenant-Id: 3f2a1b2c-3d4e-4f5a-8b6c-7d8e9f0a1b2c
X-Webhook-Delivery-Id: 0192f3c1-7f80-71a2-9b3c-4d5e6f7a8b9c

Тіло (приклад — examples/webhook-event-example.json):

JSON
{
  "delivered_attempt": 1,
  "entity_id": "0192f3c1-5d6e-7f80-9a1b-2c3d4e5f6a7b",
  "entity_type": "order",
  "event_id": "0192f3c1-6e7f-7091-8a2b-3c4d5e6f7a8b",
  "event_type": "order.approved",
  "payload": {
    "agent_id": "3f2a9b7c-1d2e-4f50-8a6b-7c8d9e0f1a2b",
    "client_id": "3f2a4d5e-6f70-4812-9a3b-4c5d6e7f8091",
    "currency": "UAH",
    "order_date": "2026-10-27",
    "order_id": "0192f3c1-5d6e-7f80-9a1b-2c3d4e5f6a7b",
    "order_number": "ORD-20261027-0042",
    "status": "approved",
    "total_amount": "12450.00"
  },
  "tenant_id": "3f2a1b2c-3d4e-4f5a-8b6c-7d8e9f0a1b2c"
}
ПолеТипОпис
event_typestringтип події
event_idUUIDідентифікатор події для вашої підписки; однаковий в усіх повторах — ключ ідемпотентності
tenant_idUUIDтенант
entity_typestring | nullorder для подій заявок; null для тестового пінгу
entity_idUUID | nullid заявки
payloadobjectдані події
delivered_attemptintномер спроби, з 1

payload подій order.*:

ПолеТипОпис
order_idUUIDid заявки
order_numberstringномер заявки
order_datestring YYYY-MM-DDдата заявки
client_idUUIDточка
agent_idUUIDторговий
statusstringпоточний статус: draft, pending, approved, rejected, in_delivery, delivered, completed, cancelled
total_amountstringсума десятковим рядком, напр. "12450.00"
currencystringвалюта, напр. UAH
manual, reasonbool, stringлише в order.status_changed

Рядків заявки (товарів) у події немає. Повний склад заявки облікова система отримує через обмін.

Тіло — компактний JSON з відсортованими ключами; не-ASCII символи екрановані як \uXXXX. Мітки часу — ISO 8601, UTC.

6.3. Відповідь, повтори, порядок

  • Відповідайте 2xx одразу, як прийняли подію (обробку робіть асинхронно). Таймаут — 15 с.
  • Будь-яка інша відповідь, таймаут чи мережева помилка — повтор за розкладом: 1 хв, 5 хв, 30 хв, 2 год, 12 год (до 6 спроб). Редиректи (3xx) не виконуються.
  • Перша спроба — протягом ~30 с після зміни заявки.
  • Доставка «принаймні один раз»: одна й та сама подія може прийти кілька разів. Дедуплікуйте за event_id (або X-Webhook-Delivery-Id).
  • Порядок не гарантується: повтори можуть прийти пізніше за новіші події. Орієнтуйтеся на payload.status.
  • Якщо зміну заявки відкочено — вебхука не буде.

6.4. Підпис

X-Webhook-Signature: t=<unix-час у секундах>,v1=<hex>
v1 = HEX( HMAC-SHA256( key = секрет підписки (UTF-8), message = "<t>.<сире тіло запиту>" ) )
  • Підписуються рівно ті байти, що прийшли в тілі. Не перекодовуйте JSON перед перевіркою.
  • t — час конкретної спроби; при повторі t і підпис нові.
  • Відкидайте запити з t, старшим за 5 хв, і порівнюйте підпис у постійному часі.
  • При заміні секрету на час переходу перевіряйте обидва секрети.
import hashlib, hmac, time

def verify_signature(raw_body: bytes, signature_header: str, secret: str, tolerance: int = 300) -> bool:
    try:
        parts = dict(item.split("=", 1) for item in signature_header.split(","))
        timestamp, received = int(parts["t"]), parts["v1"]
    except (KeyError, ValueError):
        return False
    if tolerance and abs(time.time() - timestamp) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, received)

Повний приклад на Python з Flask — examples/webhook_verify.py.