Skip to content

Webhooks

Register a webhook URL and we’ll deliver run and session events as they happen, so you don’t have to poll.

Events

EventWhen
run.completedEvery session in a run finished successfully.
run.failedThe run finished with no usable results.
run.partialThe run finished, but some sessions failed.
session.completedOne session finished (useful for multi-persona fan-out).
session.failedOne session failed.

Register

Terminal window
curl -s -X POST "$API_URL/webhooks" \
-H "Authorization: Bearer $SYNTHUSERS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/synthusers/webhook",
"events": ["run.completed", "run.failed"]
}'
# → { "id": "wh_...", "secret": "<store this now — shown once>" }

The secret is returned once. Store it — you need it to verify signatures.

Signed delivery

Every delivery carries a signature header:

X-Synthusers-Signature: t=1715900000,v1=<hex hmac-sha256>

The signed value is t + "." + raw_request_body. Verify it like this:

import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=")),
);
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(parts.v1, "hex"),
);
}

Reject any delivery whose timestamp is more than 5 minutes old.

Payload shape

Every delivery is the same envelope. The payload object depends on the event.

{
"event": "run.completed",
"customer_id": "cust_...",
"timestamp": "2026-05-15T18:24:00.000Z",
"payload": {
"run_id": "run_01H...",
"status": "completed",
"session_ids": ["sess_01H...", "sess_02H..."]
}
}

A session.completed (or session.failed) payload looks like:

{
"event": "session.completed",
"customer_id": "cust_...",
"timestamp": "2026-05-15T18:23:10.000Z",
"payload": {
"run_id": "run_01H...",
"session_id": "sess_01H...",
"persona_id": "pers_...",
"termination_kind": "success",
"turns": 12
}
}

Use the ids to fetch the report you want.

Retries

If your endpoint doesn’t return a 2xx, we retry — up to 5 attempts total. After the immediate first try, the delays are:

+30s → +5min → +30min → +2h

After the last attempt fails, the delivery is marked dead. You can inspect recent attempts (including dead ones) with GET /webhooks/:id/deliveries.

Test deliveries

Terminal window
curl -s -X POST "$API_URL/webhooks/$WH_ID/test" \
-H "Authorization: Bearer $SYNTHUSERS_API_KEY"

This sends a webhook.test event to your endpoint so you can build and test your verification code without starting a real run.