TracePass
Справочник

Webhooks

HMAC-подписана доставка за жизнения цикъл на паспортите. Retry ladder, проверка на подпис, защита срещу повторение.

TracePass прави POST на JSON-bodied събития към вашия URL, когато паспорти, extractions или supplier requests сменят състояние. Всяка доставка е HMAC-SHA-256 подписана с per-endpoint secret, който вие контролирате, повтаря се по фиксиран експоненциален ladder и носи стабилен X-TracePass-Event-Id за защита срещу повторение.

Конфигуриране на endpoint

Отворете Developer → Webhooks в таблото. Добавете вашия URL, копирайте per-endpoint signing secret (връща се веднъж при създаването — запишете го тогава, подменяйте го с delete + recreate) и изберете кои събития да доставяте. Можете да имате множество endpoints — обикновено един за staging, един за production. Бутонът Send test в таблото изпраща webhook.test envelope, за да можете да свържете receiver-а преди реалните събития да започнат да текат.

Събития

СъбитиеИзстрелва се когато
passport.publishedПаспорт преминава в status=published.
passport.suspendedПаспорт е спрян през API или UI. Публичният преглед се превключва в suspended състояние.
passport.expiredexpiresAt timestamp на публикуван паспорт изтича, например когато приключи гратисният период след прекратяване на абонамента.
passport.archivedПаспорт е архивиран. Публичният преглед връща 404 (404, а не 410 — URL се държи като никога да не е съществувал).
passport.marked_for_deletionПаспорт е маркиран за изтриване — първата от двете стъпки, с които се изтрива. Payload: passportId, GTIN и сериен номер (или identifier), markedForDeletionAt и reason, ако е подаден.
passport.deletedПаспорт, маркиран за изтриване, е изтрит. Нищо не се заличава: паспортът изчезва от API и от всичко, което е публично достъпно, сканирането на QR кода показва страница, че паспортът е прекратен, а машинночетимите формати връщат 410. Payload: passportId, GTIN и сериен номер (или identifier), deletedAt и reason, ако е подаден.
extraction.completedAI извличането приключва за паспорт. Тялото носи extraction id за последващи четения.
extraction.failedИзвличане не може да бъде завършено и се маркира като failed.
supplier.submittedДоставчик завършва token-auth supplier portal submission за един от вашите supplier requests.
epcis.event.capturedЕдно или повече EPCIS 2.0 събития са заловени за паспорт чрез capture endpoint-а.
passport.measurement.capturedЕдно или повече измервания са заловени за паспорт. Изстрелва се веднъж на POST заявка независимо от броя на записите с измервания в заявката.
scope.changedЗаконодателна промяна или коригирано правило променя извода за обхвата, датата, нивото или задълженото лице за един или повече от вашите публикувани паспорти (от Growth нагоре). Payload: runId, changedCount, byChange, passports (първите 1 000 id-та), reportUrl.
passport.identifier_correctedАдминистратор коригира заключен идентификатор в публикуван паспорт. Съдържание: passportId, key, newValue (никога старата стойност).
passport.placed_on_marketПаспорт на батерия е отбелязан като пуснат на пазара: записали сте регистрацията му в Регистъра на ЕС или API на регистър го е приел. Payload: passportId, dppId, placedOnMarketAt и registrationId, когато регистърът е върнал такъв.
passport.placement_rejectedAPI на регистър окончателно е отказал пускането на паспорт на пазара след всички повторни опити, или импортиран отчет за активност от Регистъра на ЕС показва, че регистрацията му е неуспешна.

Проверка на подписите

Всяка заявка носи следната тройка за подписване:

HeaderСтойност
X-TracePass-Signaturev1=<hex HMAC-SHA256 на `${X-TracePass-Timestamp}.${rawBody}` с вашия endpoint secret>.
X-TracePass-TimestampUnix секунди в момента на подписване. Отхвърляйте доставки, чийто timestamp е на повече от 5 минути от часовника ви — това е прозорецът за защита срещу повторение.
X-TracePass-EventИме на типа събитие (напр. passport.published). Позволява на receiver-а да dispatch-ва, без да парсва JSON.
X-TracePass-Event-IdСтабилен между повторения — деуплицирайте по него. Огледало на полето `id` в тялото.
X-TracePass-Delivery-IdPer-attempt id. Използвайте го за support корелация, не за деуплициране.

Преизчислете подписа върху ${timestamp}.${rawBody} — timestamp се конкатенира с литерален . разделител преди суровите байтове. Не сериализирайте JSON отново; whitespace различия ще обърнат HMAC. Сравнявайте с constant-time проверка.

Node / TypeScript

typescript
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.TRACEPASS_WEBHOOK_SECRET!;

// Capture the RAW body — needed for HMAC verification.
app.post("/tracepass-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const sigHeader = String(req.headers["x-tracepass-signature"] ?? "");
  const ts = String(req.headers["x-tracepass-timestamp"] ?? "");

  // 5-minute replay window.
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - Number(ts)) > 300) {
    return res.status(400).send("stale");
  }

  // Parse the v1=<hex> envelope.
  const m = /^v1=([0-9a-f]+)$/i.exec(sigHeader);
  if (!m) return res.status(400).send("malformed signature header");
  const provided = Buffer.from(m[1], "hex");

  // HMAC over `${timestamp}.${body}`.
  const expected = createHmac("sha256", SECRET)
    .update(`${ts}.`)
    .update(req.body)
    .digest();

  if (
    provided.length !== expected.length ||
    !timingSafeEqual(provided, expected)
  ) {
    return res.status(400).send("bad signature");
  }

  const event = JSON.parse(req.body.toString("utf8"));
  // ... your handler ...
  res.status(200).send("ok");
});

Python

python
import hmac, hashlib, os, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["TRACEPASS_WEBHOOK_SECRET"].encode()

@app.post("/tracepass-webhook")
def handler():
    sig_header = request.headers.get("X-TracePass-Signature", "")
    ts = request.headers.get("X-TracePass-Timestamp", "0")

    if abs(int(time.time()) - int(ts)) > 300:
        abort(400, "stale")

    if not sig_header.startswith("v1="):
        abort(400, "malformed signature header")
    provided = bytes.fromhex(sig_header[3:])

    raw = request.get_data()
    expected = hmac.new(SECRET, f"{ts}.".encode() + raw, hashlib.sha256).digest()

    if not hmac.compare_digest(provided, expected):
        abort(400, "bad signature")

    event = request.get_json()
    # ... your handler ...
    return "ok", 200

Go

go
func handler(w http.ResponseWriter, r *http.Request) {
    sigHeader := r.Header.Get("X-TracePass-Signature")
    ts        := r.Header.Get("X-TracePass-Timestamp")

    tsInt, _ := strconv.ParseInt(ts, 10, 64)
    if math.Abs(float64(time.Now().Unix()-tsInt)) > 300 {
        http.Error(w, "stale", http.StatusBadRequest); return
    }
    if !strings.HasPrefix(sigHeader, "v1=") {
        http.Error(w, "malformed signature header", http.StatusBadRequest); return
    }
    provided, err := hex.DecodeString(strings.TrimPrefix(sigHeader, "v1="))
    if err != nil {
        http.Error(w, "malformed signature header", http.StatusBadRequest); return
    }

    body, _ := io.ReadAll(r.Body)
    mac := hmac.New(sha256.New, []byte(os.Getenv("TRACEPASS_WEBHOOK_SECRET")))
    mac.Write([]byte(ts + "."))
    mac.Write(body)
    expected := mac.Sum(nil)

    if !hmac.Equal(provided, expected) {
        http.Error(w, "bad signature", http.StatusBadRequest); return
    }

    // parse body, dispatch event …
    w.WriteHeader(http.StatusOK)
}

Retry ladder

Доставката се счита за успешна, когато вашият endpoint върне 2xx в рамките на 10 секунди. Всичко останало (мрежова грешка, timeout, 4xx, 5xx) задейства повторение по този фиксиран график, общо седем опита:

ОпитЗабавяне след предишния опит
1 (първоначален)Следващото изпращане, до около минута след събитието
21 минута
35 минути
430 минути
52 часа
612 часа
7 (последен)24 часа

След опит 6 доставката се паркира в webhook event log на таблото със статус failed; можете да я повторите ръчно оттам. Запазването на история на доставките е обвързано с плана: Basic 30 дни, Starter 60, Growth 90, Scale 180, Pro 365, Enterprise 730. TTL се изчислява при insert от плана на компанията, така че смяна на плана не изтрива стари доставки със задна дата.

Защита срещу повторение

Третирайте X-TracePass-Event-Id като ключ за деуплициране. Той е огледало на полето id в тялото, повтаря се между retries нарочно (за да може вашият receiver да направи no-op, когато вече е обработил събитието) и никога не се повтаря между различни събития. SQL UNIQUE на колоната или Redis SET NX със 7-дневен TTL — и двете работят.