Webhooks
Register a webhook URL and we’ll deliver run and session events as they happen, so you don’t have to poll.
Events
| Event | When |
|---|---|
run.completed | Every session in a run finished successfully. |
run.failed | The run finished with no usable results. |
run.partial | The run finished, but some sessions failed. |
session.completed | One session finished (useful for multi-persona fan-out). |
session.failed | One session failed. |
Register
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
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.