Cargotime

Вебхуки

Получайте события отслеживания и проверяйте подлинность запросов

Вебхуки позволяют получать уведомления об изменениях отслеживания, как только они появляются в 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 и повторная сериализация могут изменить байты и сделать подпись недействительной.

Обновлено

На этой странице