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#
| Event | Ausgelöst wenn |
|---|---|
hold.approved | Die Aktion wurde freigegeben (bei Vier-Augen: nach der letzten nötigen Freigabe). |
hold.rejected | Ein Approver hat abgelehnt (immer sofort final). |
hold.expired | Die 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
statusim Payload ist maßgeblich, nicht die Ankunftsreihenfolge. - Antworten Sie schnell: quittieren Sie mit
200und verarbeiten Sie asynchron, statt im Handler zu arbeiten.