Документація API

Інтеграція біржі SAMO-TRANS у вашу TMS або CRM

Документація API

Вебхуки

Події заявок і стрічка нових пропозицій під ваш напрямок, підпис 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

Критерії мусять звужувати біржу хоча б за одним полем — підписка «на все» неможлива.

ПолеЗначення
typeCARGO або 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 значення — код регіону, для localityosmId із /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 — вмикаються знову. Перестворювати їх не потрібно.