Skip to main content
A webhook sends an HTTPS POST to your server when something happens in your workspace: a call starts, ends or fails, a call record is sealed, or a model you use is deprecated. One webhook covers every application in the workspace, Autopilot agents and streams alike. This guide shows how to create one, check that each request really came from Fonoster, and handle retries.

Before you begin

You need:
  • An HTTPS endpoint on a public host that answers POST requests. To test from your own machine, expose your local server with an HTTPS tunnel such as ngrok or cloudflared.
  • For the SDK or the CLI: an API key and Node.js 22 or later. The quickstart shows how to install both and sign in.

Choose your events

Subscribe only to the events you handle. Fonoster refuses event names it doesn’t know. Every request body uses the same envelope. Here is a call.ended event:
Events are small on purpose. They never carry a transcript, a recording or a secret. To read what was said on a call, subscribe to call_record.sealed and fetch the record it points to. Every field of every event is in Webhook events.
To tell your applications apart, filter on data.appRef. There’s one webhook per endpoint, not one per application.

Create a webhook

  1. Go to Webhooks, under Workspace.
  2. Choose Add webhook.
  3. Enter the URL, for example https://hooks.example.com/fonoster.
  4. Under Events, check the events you want. Call ended is checked for you.
  5. Choose Create webhook.
The page shows the signing secret once, starting with whsec_. Copy it, then choose I copied it.The Webhooks page in the dashboard: one endpoint, https://hooks.example.com/fonoster, subscribed to call.ended and call_record.sealed, and Recent deliveries with a delivered call.ended and a failed call_record.sealed (12 attempts, last 503), each with a Resend button.The Webhooks page in the dashboard: one endpoint, https://hooks.example.com/fonoster, subscribed to call.ended and call_record.sealed, and Recent deliveries with a delivered call.ended and a failed call_record.sealed (12 attempts, last 503), each with a Resend button.
Fonoster shows the signing secret only when you create the webhook and when you rotate it. After that you see only its last four characters. Store it in your server’s secrets right away.
Fonoster checks the URL when you save it, and again before every delivery:
  • It must start with https://.
  • Its host must be public. Private, loopback and link-local addresses and internal host names are refused.
  • Fonoster doesn’t follow redirects. A 3xx answer counts as a failed attempt.
On your own install, the operator can allow plain http:// URLs for local testing, and can allow chosen private hosts. See Install with Docker Compose.

Verify the signature

Every request carries two headers: To verify a request, compute the HMAC over the timestamp, a dot and the raw body bytes, and accept the request if it matches any of the signatures. Follow three rules:
  • Use the raw body exactly as it arrived. Parse the JSON only after the check passes.
  • Reject a timestamp more than 300 seconds away from your clock. This stops replays of old requests.
  • Compare signatures in constant time.
This receiver checks the signature, then answers right away:
In Node.js, @fonoster/common exports the same check as Webhooks.verifyWebhook(body, headers, secret). It returns true when the timestamp is fresh and one of the signatures matches.
Webhooks and streams use different secrets. A webhook’s secret starts with whsec_ and belongs to the webhook.

Handle retries and duplicates

A delivery succeeds when your endpoint answers any 2xx within 10 seconds. Answer first and do slow work afterwards. When an attempt fails (an error status, a timeout, a redirect, or no answer), Fonoster tries again. The first retry comes 10 seconds later, and each wait doubles, up to one hour between attempts. Fonoster keeps trying for 24 hours after the first attempt, then marks the delivery failed. Delivery is at least once, so plan for two things:
  • Duplicates. You can get the same event more than once. Every retry and every resend carries the same id. Store the ids you’ve handled and skip the ones you’ve seen.
  • Order. Events can arrive out of order. A call_record.sealed can arrive before the call.ended of the same call. Use createdAt and data.callRef to put them together.
If you delete a webhook, its pending deliveries fail.

Check the delivery log

The delivery log lists each delivery, newest first: the event type and id, its status (pending, succeeded or failed), how many attempts it took, the HTTP status of the last attempt (0 means no answer), and when the last and next attempts happen.
Go to Webhooks. The log is under Recent deliveries, for all of the workspace’s webhooks.

Resend a delivery

A resend sends the same event now, with the same id, a fresh timestamp and a fresh signature. Use it after you fix a receiver that was failing. If the resend fails, the delivery goes back on the retry schedule.
Under Recent deliveries, choose Resend on the delivery.

Rotate the secret

Rotate the signing secret when it may have leaked, or on a schedule. The old secret keeps working for 24 hours. During that window, every request carries two signatures, one per secret, so a receiver that checks either one keeps working while you deploy the new secret.
On Webhooks, choose Rotate secret on the webhook, then Rotate. Copy the new secret from the alert.
Deploy the new secret to your receiver before the window ends.

Change or delete a webhook

With the SDK or the CLI, you can change a webhook’s URL or its events. The dashboard can only create, rotate and delete webhooks.
  • SDK: updateWebhook({ ref, url, events }). Fields you leave out are kept.
  • CLI: fonoster webhooks:update <ref> --url <url> or -e <event>. The -e list replaces the webhook’s events.
To delete a webhook, choose Delete in the dashboard, call deleteWebhook(ref), or run fonoster webhooks:delete <ref>. fonoster webhooks:list and fonoster webhooks:get <ref> show what you have.

Coming from eventsHook

In 0.27, an Autopilot agent sent its own events through eventsHook. Workspace webhooks replace it: See Migrate from 0.27 for the other changes. For every command and method, see Webhooks in the CLI reference and the Node SDK reference.

Next steps

Call records

Fetch the record that call_record.sealed points to.

Webhook events

Every event and every field.