Skip to main content
Point your agent at one or more endpoints on your server and Fish Audio calls them when things happen: call.ended the moment a session reaches a terminal state, call.analyzed when post-call analysis settles, and, on outbound phone calls, phone_call.dial_finished as soon as the dial attempt resolves. Use them to write results into your CRM, ticketing system, or data warehouse without polling the sessions API.

Events

For a given session, call.ended is delivered before call.analyzed, and every answered production call produces both events. When analysis has nothing to work with (the caller never spoke, or analysis is disabled) or fails, call.analyzed still arrives, with analysis.status set to skipped or error and empty result sections. phone_call.dial_finished fires only for outbound calls, and it fires whether or not anyone picks up. An answered outbound call produces all three events, phone_call.dial_finished first; an outbound call that is never answered (a dial_status of busy, no_answer, or failed) produces phone_call.dial_finished and nothing else. Unanswered calls are not billed and not analyzed.
Transcripts are deliberately excluded from webhook payloads. Fetch them on demand with GET /v1/agent/sessions/{session_id}. See conversation history.

Configure the endpoint

Webhooks live in the webhooks section of your agent’s configuration. post_call is a list, so one agent can fan events out to several systems at once. Set them in the console on your agent’s Webhooks page, or via the config API:
The list is replaced as a whole on each update, so send every endpoint you want to keep. Set it to null or [] to stop deliveries. A single post_call object is still accepted and is treated as a one-element list.
Webhook settings are part of the agent configuration, so they follow the draft-and-publish flow: publish for changes to apply to production calls.

Payload

Every payload carries the event name and a session object with a fixed set of session facts. Note the field names differ from the sessions API, which uses session_id / started_at / ended_at for the same facts. call.ended adds ended_reason; call.analyzed adds the analysis entity; phone_call.dial_finished adds top-level dial_status and answered_by:
ended_reason is hangup for a call that terminated normally and error when the session failed. New values may be added as richer end causes ship. Treat unrecognized values as informational rather than rejecting the event. dial_status is answered, busy, no_answer, or failed. answered_by reports what answering-machine detection heard (human, voicemail, or unknown) and is null unless the call was answered. Both facts also appear on the session, alongside direction (inbound or outbound) and batch_call_id (reserved, always null today); these session fields are present in every webhook payload, with the dial fields null on inbound sessions. The phone_call.dial_finished snapshot is taken when the dial resolves, so on an answered call conversation_ended_at and duration_seconds are still null. The final numbers arrive with call.ended. end_user_id and metadata are echoed exactly as you set them when creating the session. Use them to correlate the event with records in your own system. branch_id is an internal configuration-lineage identifier and is safe to ignore. What lands in summary, data, and criteria_results is defined by your analysis configuration. The example shows a completed analysis. analysis.status can also be skipped (nothing to analyze: the caller never spoke, or analysis is disabled) or error (the run failed, with the cause in analysis.error); both arrive with summary: null and empty data / criteria_results. Check the status before reading results.

Verify the signature

When an entry has a secret, every request to that endpoint carries a signature header with the send time and an HMAC-SHA256 of {timestamp}.{raw_body}, keyed with that entry’s own secret:
t is the Unix time (seconds) the request was sent; each retry is signed fresh. v1 is the hex HMAC. Verify in three steps:
  1. Parse t and v1 from the header.
  2. Recompute HMAC-SHA256 over the string {t}. followed by the raw request body bytes (before any JSON parsing or re-serialization) and compare against v1 in constant time.
  3. Reject requests whose t is more than 5 minutes from your clock. Because the timestamp is inside the MAC, a replayed capture can’t be refreshed.
Reject requests with a missing or invalid signature. Without verification, anyone who discovers your endpoint URL can forge call results.
Future scheme revisions would ship under a new element (v2=…) alongside v1. Parse the elements you know and ignore the rest, as the snippets above do.

Delivery semantics

Endpoints are delivered in parallel and independently: each gets its own attempts, its own retry budget, and its own signature keyed with its own secret. An endpoint that is down and exhausts all three attempts has no effect on the others. Respond with a 2xx status within the timeout; a 500 response or a timed-out request counts as a failed attempt. Acknowledge first and process asynchronously. Slow handlers burn their own retry budget. Idempotency. At-least-once delivery means the same event can arrive more than once. Retries of one delivery carry an identical body, so dedupe call.ended and phone_call.dial_finished on (event, session.id), and call.analyzed on (event, session.id, analysis.finished_at). The extra element matters because a skipped or failed analysis can be re-run from the console: the recovered result arrives as a fresh call.analyzed with a newer finished_at, superseding the earlier one.
Preview calls made from the Builder never trigger webhooks. To test end to end, run a real session against your published agent.

Auto-ticket unresolved calls

call.analyzed closes the loop on conversations the agent couldn’t: judge every call with a success criterion, and open a ticket in your helpdesk whenever the verdict isn’t success. First give the agent’s analysis configuration a criterion that captures resolution:
analysis.criteria
Then add your endpoint to webhooks.post_call (see Configure the endpoint) and act on the verdict:
verifyWebhook is the function from Verify the signature; openTicket stands in for your helpdesk’s API. Escalating on anything but success includes unknown verdicts: the model couldn’t judge the call, which usually deserves human eyes too. Tighten the check to failure only if unknowns prove noisy. Edges worth handling:
  • Calls with nothing to analyze arrive with analysis.status: "skipped". The handler above tickets them as unjudged, so every call reaches the helpdesk without also watching call.ended. Drop that branch if silent calls don’t belong in your queue.
  • Payloads carry no transcript. To include one in the ticket, fetch GET /v1/agent/sessions/{session_id} from your handler. See conversation history.
  • Set end_user_id and metadata when creating sessions so tickets attach to the right customer record without a lookup.
Post-call ticketing is silent: the caller has already hung up. When the caller should leave the call holding a ticket number, have the agent open it mid-call with a webhook tool, and keep this recipe as the safety net behind it.

Going further

Post-call analysis

Define the summary, data fields, and criteria that call.analyzed delivers.

Conversation history

Fetch the transcript, tool timeline, and recordings by session_id.

Versions & publishing

How configuration changes (including webhooks) go live.

Agent configuration

The full config schema behind PATCH /v1/agent/agents/{agent_id}/config.