For Developers
Agents

Post-call webhooks

Configure an agent to send signed call results to your HTTPS endpoint, and check delivery history.

Each agent can send signed JSON events to one HTTPS address when its calls end. Events carry the transcript, summary, analysis and recording links. For a step-by-step setup, see Receive post-call webhooks.

Every route here needs a key with write permission, including the read routes. A key with only read permission gets 403 FORBIDDEN, because saved addresses can hold tokens and aren't masked.

A missing or invalid key returns 401. :id must be the agent's UUID, or the route returns 400. An agent in another account returns 404.

A body or :id that fails validation returns 400 with the code VALIDATION_ERROR, and details.issues lists the problems. A rejected address or unavailable delivery returns the same code with the reason in error. See Errors.

EndpointRequest
Get webhook settingsGET/v1/agents/:id/post-call-webhook
Save webhook settingsPUT/v1/agents/:id/post-call-webhook
List deliveriesGET/v1/agents/:id/post-call-webhook/deliveries
Send a test eventPOST/v1/agents/:id/post-call-webhook/test

The webhook settings object

Attributes

deliveryAvailablebooleanrequired
Whether webhook delivery is available in this environment. When false, config is null, deliveries is empty, and saving or testing returns 400.
configobject | nullrequired
The agent's settings. null until you first save them.
config.urlstring
The address events are sent to.
config.enabledboolean
Whether call events are sent.
config.versionstring
UUID of the current settings. Send it as expectedVersion on your next change or test.
config.hasSigningSecretboolean
true for any saved configuration. The secret itself is never returned.
config.updatedAtstring
When the settings last changed, as an ISO 8601 timestamp.
deliveriesarrayrequired
The 10 most recent delivery receipts, newest first.

The delivery receipt object

A receipt records what happened to one event. It holds metadata only, never the transcript, your endpoint's response or the address. Receipts are removed after 30 days.

Attributes

idstringrequired
The event's UUID. It matches the X-Synthesys-Event-Id header and the payload's id.
call_idstring | nullrequired
The call's UUID. null for webhook.test.
event_typestringrequired
call.completed, call.updated, call.recording_ready or webhook.test.
statusstringrequired
pending, delivered, failed or canceled.
attemptsintegerrequired
How many times the event was sent.
last_http_statusinteger | nullrequired
The HTTP status from the latest attempt. null when no status was received.
last_errorstring | nullrequired
A short description of the latest failure. See Delivery errors.
created_atstringrequired
When the event was created, as an ISO 8601 timestamp.
delivered_atstring | nullrequired
When your endpoint accepted the event.

Get webhook settings

GET/v1/agents/:id/post-call-webhookwrite key

Returns the agent's settings and its 10 most recent delivery receipts. A read key gets 403.

Request
curl https://backend.synthesys.app/api/v1/agents/3fa85f64-5717-4562-b3fc-2c963f66afa6/post-call-webhook \
  -H "SYNTH_API_KEY: YOUR_API_KEY"
Response
{
  "success": true,
  "data": {
    "deliveryAvailable": true,
    "config": {
      "url": "https://example.com/webhooks/synthesys",
      "enabled": true,
      "version": "5d0c7b1e-2f64-4a39-b8e7-1c9a6f3d2e80",
      "hasSigningSecret": true,
      "updatedAt": "2026-10-30T12:00:00+00:00"
    },
    "deliveries": [
      {
        "id": "e7a1c2d4-6b3f-4e58-9a07-3d1f8c5b6a29",
        "call_id": "b1f4d8a3-7c2e-4d96-8e15-0a6c3f9b2d47",
        "event_type": "call.completed",
        "status": "delivered",
        "attempts": 1,
        "last_http_status": 200,
        "last_error": null,
        "created_at": "2026-10-31T12:00:05+00:00",
        "delivered_at": "2026-10-31T12:00:07+00:00"
      }
    ]
  }
}

Save webhook settings

PUT/v1/agents/:id/post-call-webhookwrite key

Creates or updates the agent's settings. Returns the same shape as Get webhook settings.

Body

urlstringrequired
Your endpoint, up to 2048 characters. It must use HTTPS on port 443, with no credentials or fragment. It must resolve only to public internet addresses. This is checked on the first save, when the address changes and when you enable the webhook, so you can disable a webhook whose address no longer resolves.
enabledbooleanrequired
Whether to send call events.
expectedVersionstring | nullrequired
null on the first save. After that, the version from your latest response.

Other body fields are rejected. You can't supply your own signing secret.

The first save generates the secret. Later saves keep it, and it can't be rotated. To copy it, open the agent's settings in the dashboard and choose Reveal signing secret. Only workspace admins can do this. No /v1 route returns it.

Changing url or enabled issues a new version and cancels events still pending. Calls that ended before the change don't send events, so nothing is backfilled. Sending the same values again changes nothing.

StatusWhen
400The body or address is invalid, the hostname can't be resolved, or delivery is unavailable in this environment.
403The key lacks write permission, or its owner has the viewer role, whatever the key's permissions.
404The agent doesn't exist or belongs to another account.
409expectedVersion is out of date. Read the settings again and retry.
Request
curl -X PUT https://backend.synthesys.app/api/v1/agents/3fa85f64-5717-4562-b3fc-2c963f66afa6/post-call-webhook \
  -H "SYNTH_API_KEY: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/webhooks/synthesys", "enabled": true, "expectedVersion": null}'
Response
{
  "success": true,
  "data": {
    "deliveryAvailable": true,
    "config": {
      "url": "https://example.com/webhooks/synthesys",
      "enabled": true,
      "version": "5d0c7b1e-2f64-4a39-b8e7-1c9a6f3d2e80",
      "hasSigningSecret": true,
      "updatedAt": "2026-10-30T12:00:00+00:00"
    },
    "deliveries": []
  }
}
Stale version (409)
{
  "success": false,
  "error": "Settings changed. Reload before saving.",
  "code": "CONFLICT",
  "requestId": "0f6b2c9e-1d47-4a83-b5e2-7c3a9d1f4e60",
  "timestamp": "2026-10-31T12:00:00.000Z"
}

List deliveries

GET/v1/agents/:id/post-call-webhook/deliverieswrite key

Returns the agent's 10 most recent delivery receipts, newest first. This route has no pagination, so you can't read older receipts. A read key gets 403.

Request
curl https://backend.synthesys.app/api/v1/agents/3fa85f64-5717-4562-b3fc-2c963f66afa6/post-call-webhook/deliveries \
  -H "SYNTH_API_KEY: YOUR_API_KEY"
Response
{
  "success": true,
  "data": {
    "deliveryAvailable": true,
    "deliveries": [
      {
        "id": "a2c9e4f1-5d38-4b76-8f02-6e1b7d3c9a58",
        "call_id": "b1f4d8a3-7c2e-4d96-8e15-0a6c3f9b2d47",
        "event_type": "call.recording_ready",
        "status": "pending",
        "attempts": 2,
        "last_http_status": 503,
        "last_error": "Endpoint returned HTTP 503",
        "created_at": "2026-10-31T12:03:00+00:00",
        "delivered_at": null
      }
    ]
  }
}

Send a test event

POST/v1/agents/:id/post-call-webhook/testwrite key

Queues a webhook.test event with synthetic data. No call is placed, and it works while the webhook is disabled.

Requests within the same clock minute reuse one event per settings version. They return the same deliveryId and don't send again. Check the outcome with List deliveries.

Body

expectedVersionstringrequired
The current version from your settings.
StatusWhen
400The body is invalid, or delivery is unavailable in this environment.
403The key lacks write permission, or its owner has the viewer role, whatever the key's permissions.
404The agent doesn't exist or belongs to another account.
409expectedVersion is out of date, or the webhook has never been saved.
Request
curl -X POST https://backend.synthesys.app/api/v1/agents/3fa85f64-5717-4562-b3fc-2c963f66afa6/post-call-webhook/test \
  -H "SYNTH_API_KEY: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"expectedVersion": "5d0c7b1e-2f64-4a39-b8e7-1c9a6f3d2e80"}'
Response
{
  "success": true,
  "data": { "deliveryId": "d4b8f1a6-3e92-4c57-a0d1-8f2e6b7c9a13" }
}

Event types

An event is sent for a call with this agent once the call's status is ended or failed, including test calls. The webhook must be enabled, and the call must have ended after the settings last changed. Calls older than 30 days don't produce events.

EventWhen it is sent
call.completedThe first event for a call. It waits up to 60 seconds for the transcript and end time, and is sent as soon as both are saved.
call.updatedThe call's details changed after call.completed was picked up for sending.
call.recording_readyA recording became ready after call.completed was picked up for sending.
webhook.testYou called Send a test event.

A recording that is ready before call.completed is sent is included in it. A call that never gets a transcript, such as an unanswered call, still sends call.completed after the wait. Its transcript and endedAt can be null.

Every event carries a full snapshot of the call, not a change list. Events can arrive late or out of order, so use createdAt to find the newest snapshot.

Event payload

Every event is a POST with a JSON body. Values that aren't available are null.

Body

idstringrequired
The event's UUID. It stays the same across retries.
typestringrequired
The event type.
versionintegerrequired
The payload version, currently 1.
createdAtstringrequired
When the event was created, as an ISO 8601 timestamp.
testbooleanrequired
true only for webhook.test.
callobjectrequired
A snapshot of the call.

call

idstringrequired
The call's UUID. For webhook.test, a random UUID that matches no call.
agentIdstringrequired
The agent's UUID.
statusstringrequired
ended or failed.
directionstring | nullrequired
inbound or outbound.
typestring | nullrequired
The call type, such as inboundPhoneCall, outboundPhoneCall, webCall or websocketCall. test for webhook.test. See Value mapping.
isTestCallboolean | nullrequired
Whether this is a test call.
customerPhonestring | nullrequired
The customer's phone number.
startedAtstring | nullrequired
When the call started.
endedAtstring | nullrequired
When the call ended.
durationSecondsnumber | nullrequired
The call's length, in seconds.
endedReasonstring | nullrequired
Why the call ended. Treat this as descriptive text, because the set of values can change. See Value mapping.
transcriptstring | nullrequired
The full transcript.
summarystring | nullrequired
A summary of the call.
analysisobject | nullrequired
Post-call analysis. Its keys depend on the call and the agent's settings.
recordingsarrayrequired
The call's ready recordings. Empty when there are none.
recordings[].idstringrequired
The recording's UUID.
recordings[].urlstring | nullrequired
A signed download link. null if the recording was deleted or is unavailable.
recordings[].expiresAtstring | nullrequired
When the link expires, one hour after it was created. Each send creates a new link.
JSON
{
  "id": "e7a1c2d4-6b3f-4e58-9a07-3d1f8c5b6a29",
  "type": "call.completed",
  "version": 1,
  "createdAt": "2026-10-31T12:00:05.123456+00:00",
  "test": false,
  "call": {
    "id": "b1f4d8a3-7c2e-4d96-8e15-0a6c3f9b2d47",
    "agentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "status": "ended",
    "direction": "outbound",
    "type": "outboundPhoneCall",
    "isTestCall": false,
    "customerPhone": "+12025550123",
    "startedAt": "2026-10-31T11:57:30+00:00",
    "endedAt": "2026-10-31T12:00:02+00:00",
    "durationSeconds": 152,
    "endedReason": "customer-ended-call",
    "transcript": "AI: Hello, this is Sarah.\nUser: Hi Sarah.",
    "summary": "The customer booked a demo for Tuesday.",
    "analysis": { "successEvaluation": "true" },
    "recordings": [
      {
        "id": "f3a7c1e9-2b64-4d08-b5f1-9c8e7d6a3b12",
        "url": "https://example.com/signed/recording.mp3",
        "expiresAt": "2026-10-31T13:00:07.000Z"
      }
    ]
  }
}

The webhook.test body has test: true. Its call has direction set to outbound, type and endedReason set to test, isTestCall set to true, a 30-second duration, a null customerPhone and no recordings.

Value mapping

call.type and call.endedReason never name a specific voice or phone company. Any part of a value that would do so uses the word provider instead, so a failed connection can read provider-failed-to-connect-call. Other values, such as customer-ended-call, are unchanged.

Call logs and recordings use the same values, so you can compare them directly.

Request headers

HeaderValue
X-Synthesys-Event-IdThe event's UUID. Use it to remove duplicates.
X-Synthesys-TimestampWhen this attempt was signed, in Unix seconds. It changes on each retry.
X-Synthesys-Signaturev1= followed by a lowercase hex HMAC-SHA256 signature.
Content-Typeapplication/json
User-AgentSynthesys-Webhook/1.0

Verify the signature

Compute an HMAC-SHA256 over {timestamp}.{rawRequestBody}. Use your full signing secret as the key, including its whsec_ prefix. Verify the raw bytes before parsing the JSON. Use a constant-time comparison, and reject timestamps more than five minutes old.

Verify a webhook
import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody is the request body as received, before JSON parsing.
export function verifyWebhook(rawBody, timestamp, signature, secret) {
  if (!timestamp || !signature) return false;
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!(age <= 300)) return false;

  const digest = createHmac('sha256', secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest('hex');
  const expected = Buffer.from(`v1=${digest}`);
  const received = Buffer.from(signature);
  return expected.length === received.length && timingSafeEqual(expected, received);
}

Retries

Return a 2xx status within 10 seconds. An event is sent up to five times in total. Retries use exponential backoff with jitter, and the delay between attempts is capped at about 15 minutes. If the fifth send fails, the event is marked failed.

Result of an attemptWhat happens
2xxMarked delivered.
429 or 5xxRetried.
Timeout or network failureRetried.
Any other status, including 3xx and other 4xxMarked failed with no retry. Redirects are not followed.
Address no longer resolves only to public addressesMarked failed with no retry.

Delivery is at least once, so your endpoint can receive the same event twice. Remove duplicates by id, and record an event as processed only after you have applied it. The event's id and call snapshot stay the same on retries. The timestamp, signature and recording links are refreshed.

Before each attempt, the current settings are checked. If they changed, or the webhook was disabled, a pending event is marked canceled and isn't sent. webhook.test events still send while disabled.

There is no route to resend an event. Use Call logs to fetch results you missed.

Delivery errors

These are the values you can see in last_error.

ValueMeaning
Endpoint returned HTTP {status}Your endpoint returned a non-2xx status.
Delivery failed; check endpoint availabilityA connection failure, timeout or other error stopped the send.
Destination is not a public HTTPS endpointThe address no longer passes the address checks.
Settings changed before deliveryThe settings changed while the event was pending.
Webhook disabled or settings changedThe webhook was disabled, changed or removed before the attempt.
Delivery job ended before confirmationDelivery stopped before the result was confirmed. Your endpoint may or may not have received it.