Webhook Mode
Webhook mode gives you the easiest path to “our backend decides what the caller hears.”
Saperly handles telephony, transcription, and speech output. Your server handles the decision.
compliance_enabled: true on the line. With the flag off, TCPA, GDPR, and local telecom compliance are the customer’s responsibility. See Compliance and Consent for the full liability framing and how to opt in.Create a webhook line
Event flow
Signature verification
Every outbound Saperly webhook is signed with HMAC-SHA256 so your receiver can confirm the request came from Saperly and has not been tampered with or replayed.
Headers sent with every webhook
The payload body also includes "line_id": "<uuid>" so a single webhook endpoint can serve multiple lines and look up the correct signing secret per request.
Verify with the TypeScript SDK
Verify with the Python SDK
Replay defense
The signature alone does not defeat replay attacks — an attacker who captured a valid request once could replay it later. To close that gap your receiver MUST:
- Reject any request whose
x-saperly-timestampis more than 5 minutes away from your server’s current time. The SDK helpers do this by default viaclockToleranceSec/clock_tolerance_seconds. - Cache every
x-saperly-delivery-idyou accept for at least 5 minutes and reject duplicates. The SDK helpers cannot do this for you because the cache is receiver-side state.
Rotating the signing secret
Your line’s signing secret lives server-side and is minted during line creation. To rotate it (for example, after a suspected leak):
Rotation is a two-call flow:
- First call stages a new secret into
webhook_secret_next. The current secret keeps signing traffic so existing receivers keep verifying. Deploy the returned secret to your receivers as a second acceptable secret. - Second call ≥24 hours later promotes
next → current(new traffic signs with the new secret) and mints a freshnext. You have a full day to roll the new secret out to every receiver before signed traffic actually changes.
Response:
promoted_previous_next is true when the prior next secret was aged >24h and got promoted to current as part of this call. The secret is returned exactly once — there is no read endpoint. Losing the returned value means rotating again.
Event types
Events sent to your webhook_url
Events sent to your status_callback_url
Main event you need to handle
Minimal valid response
Response rules that matter
- return JSON
- return quickly
- keep the first response short
- handle non-
messageevents without trying to speak back
If your server is slow or broken, the caller feels it immediately. This is the whole game.
Minimal handler
Handle errors gracefully
message events. If your server returns non-200 or times out, the delivery is marked failed in the webhook log and the caller hears silence.message events, return a streaming NDJSON response to get a longer budget: 15 seconds per line and 5 minutes total.Inspect failures:
Streaming responses (NDJSON)
For lower perceived latency, return streaming NDJSON instead of a single JSON object. Each line is a complete JSON object:
The caller hears each chunk as it arrives, reducing time to first word.
Security practices
- Validate the
call_idexists by checking your records or GET /api/v1/calls/{id} - Use HTTPS exclusively for your webhook URL
- Return responses within 5 seconds (call lifecycle and SMS) or 10 seconds (message events)
Status callbacks
If you set status_callback_url, Saperly POSTs lifecycle events (call_incoming for inbound, call_outgoing for outbound) to that URL separately from the main webhook. See the event table above for payload shape. Use this for logging and analytics without mixing it into your response logic.
Related guides
Read Audio Mode if you need raw audio. The Fern docs API reference still covers the underlying line and call endpoints.
What to log on day one
Log these for every call:
- event type
- call ID
- caller number
- line ID when present
- model latency on your side
- final text returned
This saves hours when the first real user says “the bot sounded weird.”
