tokyntraildocs

Webhooks

Fachlich: Webhooks sind der Rückkanal für asynchrone Prozesse — Ihr System erfährt sofort, wenn ein Mensch entschieden hat oder eine Frist abgelaufen ist, ohne zu pollen. Konfiguration unter Einstellungen → Webhook: ein Endpoint pro Organisation; das Secret wird genau einmal angezeigt.

Events#

EventAusgelöst wenn
hold.approvedDie Aktion wurde freigegeben (bei Vier-Augen: nach der letzten nötigen Freigabe).
hold.rejectedEin Approver hat abgelehnt (immer sofort final).
hold.expiredDie Frist ist abgelaufen (ggf. nach Durchlaufen einer Eskalationskette).

Das sind alle Events — es gibt bewusst kein hold.created (die POST-Antwort enthält bereits alles) und kein hold.executed/hold.failed: Das Ausführungsergebnis von Proxy-Aktionen fragen Sie bei Bedarf über GET /holds/:id ab.

Payload#

POST <ihr-endpoint> · Header: X-TokynTrail-Signature (HMAC-SHA256, hex, über den rohen Body)
{
  "event": "hold.approved",
  "created_at": "2026-07-20T08:24:31Z",
  "data": {
    "id": "tt_pa_019f7c08…",
    "status": "approved",
    "tool_name": "send_payment",
    "summary": "send_payment: amount=4900, currency=EUR",
    "agent": { "id": "billing-agent" },
    "connection": "SDK",
    "decided_at": "2026-07-20T08:24:31Z",
    "decision": { "verdict": "approve", "reason": null, "channel": "inbox" }
  }
}

Der Signatur-Header heißt X-TokynTrail-Signature.

Signatur prüfen — immer#

Verwerfen Sie jeden Request mit ungültiger Signatur. Wichtig: über den rohen Body prüfen, vor jedem JSON-Parsing.

Mit dem SDK
import { verifyWebhookSignature } from '@tokyn-trail/sdk';

app.post('/tokyn-trail-webhook', express.raw({ type: '*/*' }), (req, res) => {
  const ok = verifyWebhookSignature(
    req.body.toString('utf8'),
    req.header('X-TokynTrail-Signature') ?? '',
    process.env.TOKYN_TRAIL_WEBHOOK_SECRET!,
  );
  if (!ok) return res.status(401).end();

  const { event, data } = JSON.parse(req.body.toString('utf8'));
  // …
  res.status(200).end();
});
Ohne SDK (Node)
const expected = crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));

Zustellsemantik#

  • At-least-once: Zustellung über die Job-Queue mit bis zu 25 Versuchen und exponentiellem Backoff (Sekunden bis Stunden zwischen den Versuchen). Machen Sie Ihren Handler idempotent auf data.id.
  • 2xx = quittiert. Jede andere Antwort — und jeder Versuch, der länger als 10 Sekunden dauert — zählt als Fehlschlag und wird erneut zugestellt; nach Ausschöpfen der Versuche landet die Zustellung im Dead-Letter.
  • Reihenfolge ist nicht garantiert — der status im Payload ist maßgeblich, nicht die Ankunftsreihenfolge.
  • Antworten Sie schnell: quittieren Sie mit 200 und verarbeiten Sie asynchron, statt im Handler zu arbeiten.