Вебхуки
Получайте события отслеживания и проверяйте подлинность запросов
Вебхуки позволяют получать уведомления об изменениях отслеживания, как только они появляются в Cargotime. Сервис отправляет обычный JSON в POST-запросе на указанный вами callback_url.
Создайте вебхук через POST /v2/webhooks или в личном кабинете.
Типы событий
Актуальные типы событий и их описания возвращает GET /v2/webhook-event-types. Используйте этот список при настройке подписки. Например, доступны уведомления об изменении ожидаемой даты прибытия (tracking.eta_update), истории перемещений (tracking.history_update) и доставке (tracking.delivered).
Если при создании вебхука не передать event_types, на callback_url будут приходить все публичные типы событий из GET /v2/webhook-event-types.
tracking.test — отдельное служебное событие для проверки доставки. На него нельзя подписаться: оно отправляется только при вызове POST /v2/webhooks/{id}/test, с пустым объектом data и resource: null.
Секрет вебхука
При создании вебхука API возвращает secret для проверки подписи входящих событий. Сохраните секрет из ответа на создание вебхука. API возвращает secret только при создании или при обновлении с regenerate_secret: true. Сам секрет можно использовать для проверки всех событий этого вебхука, пока он не будет заменён.
Если секрет потерян, создайте новый через PATCH /v2/webhooks/{id} с regenerate_secret: true и обновите его в своём обработчике.
Правила доставки
- Подтвердите получение HTTP-ответом со статусом
2xx. Если обработка занимает время, сначала сохраните событие в надёжную очередь, верните ответ и выполните обработку отдельно после отправки ответа. - Cargotime ожидает ответ до 10 секунд.
- После
408,409,425,429,5xx, таймаутов, ошибок DNS или сети доставка повторяется. Всего — не более пяти попыток. - После
429учитывается заголовокRetry-After, если он передан. - После
400,401,403,404и422повторной отправки нет.
Идентификаторы событий и попыток доставки
x-cargotime-event-id — идентификатор события, совпадающий с payload.id. Используйте его, чтобы отличать события и не обрабатывать одно событие повторно: это обеспечивает идемпотентность обработчика.
x-cargotime-delivery-id — идентификатор попытки доставки. Он помогает различать попытки в журнале запросов. Одно событие может прийти несколько раз с одним event-id, но разными delivery-id.
Если событие уже успешно обработано, ответьте на повторную доставку статусом 2xx. Согласуйте сохранение идентификатора с обработкой данных так, чтобы сбой не приводил к потере события или повторному выполнению операции.
Проверка подписи
Проверка подписи не обязательна, но настоятельно рекомендуется: она позволяет убедиться, что событие отправил Cargotime и тело запроса не изменилось. Тело запроса — обычный JSON. Подпись не шифрует и не кодирует его.
Используйте secret вашего вебхука. Подпись в x-cargotime-signature — HMAC SHA-256 от timestamp + '.' + rawBody, с префиксом sha256=. Метка времени передаётся в x-cargotime-timestamp, а rawBody — исходное тело запроса без изменений.
Примеры ниже проверяют подпись и допустимое отклонение времени в 5 минут. Для Node.js установите express, для Python — fastapi. Функцию постановки события в очередь замените своей реализацией. В примере она только выводит данные.
// Проверяем время и подпись события с помощью секрета вебхука.
import crypto from "node:crypto";
import express from "express";
const app = express();
const secret = process.env.CARGOTIME_WEBHOOK_SECRET;
const MAX_SKEW_SECONDS = 5 * 60;
if (!secret) {
throw new Error("CARGOTIME_WEBHOOK_SECRET is not set");
}
function queueWebhook(payload: unknown, eventId: string): void {
// Здесь сохраните событие в очередь; вывод в журнал — только пример.
console.log("accepted", eventId, payload);
}
app.post("/webhooks/cargotime", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.header("x-cargotime-timestamp");
const signature = req.header("x-cargotime-signature");
const eventId = req.header("x-cargotime-event-id");
if (!timestamp || !signature || !eventId) {
return res.sendStatus(401);
}
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > MAX_SKEW_SECONDS) {
return res.sendStatus(401);
}
// Вычисляем подпись по исходному телу, не сериализуя JSON повторно.
const expected =
"sha256=" + crypto.createHmac("sha256", secret).update(timestamp).update(".").update(req.body).digest("hex");
const signatureBuffer = Buffer.from(signature);
const expectedBuffer = Buffer.from(expected);
if (signatureBuffer.length !== expectedBuffer.length || !crypto.timingSafeEqual(signatureBuffer, expectedBuffer)) {
return res.sendStatus(401);
}
queueWebhook(JSON.parse(req.body.toString("utf8")), eventId);
return res.sendStatus(200);
});Вычисляйте подпись по исходному телу запроса. Разбор JSON и повторная сериализация могут изменить байты и сделать подпись недействительной.
Обновлено