TracePass
Справочник

Webhooks

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

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 на публикуван паспорт изтича (дневен cron). Използва се от revenue-protection moat след 30-дневния cancellation grace период.
passport.archivedПаспорт е архивиран. Публичният преглед връща 404 (404, а не 410 — URL се държи като никога да не е съществувал).
extraction.completedMulti-agent AI extraction pipeline-ът завършва за паспорт. Тялото носи extraction id за последващи четения.
extraction.failedExtraction сесия е извън 1h resume прозореца без възстановяване — orchestrator-ът я маркира като failed.
supplier.submittedДоставчик завършва token-auth supplier portal submission за един от вашите supplier requests.

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

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

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 часа
6 (последен)12 часа

След опит 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 — и двете работят.