Вебхуки
Події заявок і стрічка нових пропозицій під ваш напрямок, підпис X-Samo-Signature, повтори.
Вебхук — це HTTPS-адреса на вашому боці, куди SAMO-TRANS сам надсилає подію, щойно вона стається. Це дешевше й швидше за опитування біржі за розкладом.
Події
| Подія | Коли надсилається |
|---|---|
proposal.created | створено вашу заявку (з сайту або через API) |
proposal.updated | вашу заявку змінено |
proposal.deleted | вашу заявку видалено |
proposal.matched | на біржі з'явилася заявка іншої компанії, що підходить під ваші критерії |
Перші три стосуються ваших власних заявок і корисні для синхронізації стану, якщо заявки редагують і в кабінеті, і через API.
proposal.matched — навпаки: це стрічка нових пропозицій під ваш напрямок. Замість того щоб
щохвилини опитувати пошук, ви отримуєте заявку в момент її появи.
Підключення
Вебхуки налаштовує власник компанії в кабінеті: Налаштування → API, блок вебхуків під переліком ключів. Через ключ API створити вебхук не можна — це навмисно, керування підписками залишається за людиною з доступом до кабінету.
При створенні задаються:
- URL — тільки
https://. Адреса має бути публічною: приватні, локальні та службові адреси (зокрема169.254.169.254) відхиляються. - Події — одна або кілька зі списку вище.
- Критерії — обов'язкові й доступні лише для
proposal.matched.
Секрет показується один раз одразу після створення. Збережіть його — саме ним перевіряється підпис. Відновити секрет неможливо, лише створити вебхук наново.
Критерії для proposal.matched
Критерії мусять звужувати біржу хоча б за одним полем — підписка «на все» неможлива.
| Поле | Значення |
|---|---|
type | CARGO або TRANSPORT |
carTypes | коди кузовів (до 50) — з довідника /reference/car-types |
weight | { "min": …, "max": … }, тонни — потрібна хоча б одна межа |
volume | { "min": …, "max": … }, м³ |
date | { "start": "…", "end": "…" } — дата завантаження |
from, to | до 25 локацій у кожному напрямку |
Локація описується так:
{ "type": "country", "value": "PL" }
{ "type": "region", "value": "UA-59", "countryCode": "UA" }
{ "type": "locality", "value": "3678531", "countryCode": "UA" }
Для region значення — код регіону, для locality — osmId із
/reference та /localities/search.
Приклад: «вантажі з Волині до Польщі, від 15 тонн, тентовані»:
{
"type": "CARGO",
"from": [{ "type": "region", "value": "UA-07", "countryCode": "UA" }],
"to": [{ "type": "country", "value": "PL" }],
"weight": { "min": 15 },
"carTypes": ["tent"]
}
Формат доставки
Кожна подія — окремий POST із тілом:
{
"event": "proposal.matched",
"timestamp": "2026-08-05T09:14:22.481Z",
"data": { "id": "…", "type": "CARGO", "route": { … }, "…": "…" }
}
Заголовки:
Content-Type: application/json
User-Agent: SAMO-TRANS-Webhooks/1
X-Samo-Event: proposal.matched
X-Samo-Webhook-Id: <id вебхука>
X-Samo-Signature: sha256=<hex>
У події proposal.matched контакти вирізано: це чужа заявка, і доступ до контактів
залежить від вашого тарифу. Отримавши подію, заберіть повну картку своїм ключем —
GET /proposals/{id}.
Для proposal.deleted поле data містить лише id.
Перевірка підпису
Підпис — HMAC-SHA256 від сирого тіла запиту секретом вебхука. Рахуйте його до розбору JSON: будь-яка нормалізація (зміна порядку ключів, пробіли) зламає збіг.
const crypto = require("crypto");
function isValidSignature(rawBody, headerValue, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const received = String(headerValue || "").replace(/^sha256=/, "");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(received, "hex");
// Порівняння сталого часу — щоб не дати підібрати підпис за таймінгом.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: підпис рахується по сирому тілу, тому express.json() тут не підходить
app.post("/samo/webhook", express.raw({ type: "application/json" }), (req, res) => {
if (!isValidSignature(req.body, req.get("X-Samo-Signature"), process.env.SAMO_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
// Швидко підтвердьте прийом і обробляйте асинхронно — на відповідь є 10 секунд.
enqueue(event);
res.sendStatus(200);
});
<?php
$raw = file_get_contents('php://input');
$expected = hash_hmac('sha256', $raw, getenv('SAMO_WEBHOOK_SECRET'));
$received = preg_replace('/^sha256=/', '', $_SERVER['HTTP_X_SAMO_SIGNATURE'] ?? '');
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
// ... поставити в чергу ...
http_response_code(200);
Повтори й вимоги до вашого боку
- Успіхом вважається будь-яка відповідь
2xx. Усе інше — помилка. - Редиректи не виконуються:
3xxвважається помилкою, вказуйте кінцеву адресу. - Таймаут — 10 секунд. Відповідайте одразу, а обробку виконуйте у фоні.
- До 5 спроб з експоненційною затримкою, починаючи з 5 секунд.
- Доставка гарантує «не менше одного разу»: та сама подія може прийти двічі. Робіть обробник
ідемпотентним — дедуплікуйте за парою
event+data.id+timestamp. - Порядок подій не гарантується. Якщо для вас важливий фінальний стан заявки — перезапитайте її
через
GET /proposals/{id}.
Життєвий цикл
- Вебхук можна видалити в кабінеті; доставки припиняються негайно.
- При переході компанії на тариф без API всі вебхуки автоматично деактивуються, а після повернення на PREMIUM — вмикаються знову. Перестворювати їх не потрібно.