Вебхуки
Вихідні вебхуки 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. Формат запиту
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):
{
"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_type | string | тип події |
event_id | UUID | ідентифікатор події для вашої підписки; однаковий в усіх повторах — ключ ідемпотентності |
tenant_id | UUID | тенант |
entity_type | string | null | order для подій заявок; null для тестового пінгу |
entity_id | UUID | null | id заявки |
payload | object | дані події |
delivered_attempt | int | номер спроби, з 1 |
payload подій order.*:
| Поле | Тип | Опис |
|---|---|---|
order_id | UUID | id заявки |
order_number | string | номер заявки |
order_date | string YYYY-MM-DD | дата заявки |
client_id | UUID | точка |
agent_id | UUID | торговий |
status | string | поточний статус: draft, pending, approved, rejected, in_delivery, delivered, completed, cancelled |
total_amount | string | сума десятковим рядком, напр. "12450.00" |
currency | string | валюта, напр. UAH |
manual, reason | bool, 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)// Express: app.post("/apexsfa/webhook", express.raw({ type: "application/json" }), handler)
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifySignature(rawBody, header, secret, tolerance = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
const ts = Number(parts.t);
if (!ts || !parts.v1) return false;
if (tolerance && Math.abs(Date.now() / 1000 - ts) > tolerance) return false;
const expected = createHmac("sha256", secret).update(`${ts}.`).update(rawBody).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(parts.v1);
return a.length === b.length && timingSafeEqual(a, b);
}# Перевірка вручну: тіло збережено у body.json, заголовок — у змінній SIG_HEADER
TS=${SIG_HEADER#t=}; TS=${TS%%,*}
printf '%s.' "$TS" | cat - body.json | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}'
# результат має збігтися з v1=… із заголовкаПовний приклад на Python з Flask — examples/webhook_verify.py.