> For a complete page index, fetch https://docs.synthflow.ai/llms.txt. For full documentation content, fetch https://docs.synthflow.ai/llms-full.txt.

# Webhooks

> Set up post-call and inbound webhooks to push call outcomes to your systems or route incoming calls programmatically.

Webhooks push call data to your endpoint in real time so you do not need to poll for updates. Use them to log calls in your CRM, trigger downstream automations, and store transcripts or extracted outcomes for reporting.

Validate webhook authenticity with HMAC signature verification. See [webhook security](/security#webhook-security) for setup details. To inspect delivery status, payloads, and HTTP responses, use [Webhook logs](/logs#webhook-logs) in the Synthflow dashboard.

## Webhook types

Synthflow supports two webhook types:

| Type      | When it fires                            | What you configure                                                                                                                                                       | What you do with it                                     |
| --------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------- |
| Post-call | After a call ends                        | `external_webhook_url` in your [Make a call](/api-reference/platform-api/calls/voice-call) request                                                                       | Receive transcript, status, actions, and analytics      |
| Inbound   | Within \~10s of an inbound call starting | `inbound_call_webhook_url` on the agent ([create](/api-reference/platform-api/agents/create-assistant) or [update](/api-reference/platform-api/agents/update-assistant)) | Assign or reroute agents, inject variables and metadata |

## Post-call webhook

When a call completes, Synthflow sends a structured payload to the `external_webhook_url` you provide in the call request.

Add a webhook URL to the request in order to receive information after the call.

### Setting up a Webhook

* **Specify the Webhook URL**: In your API request body, include the `external_webhook_url` key with the URL of your webhook receiver.
* **Test with the webhook:** For development and testing purposes, you can use webhook.site to simulate receiving webhook data.

### API Request Example

Include the following key-value pair in your API request body:

```json
"external_webhook_url": "www.exmaple.com/webhool_url"
```

### Webhook Response Example

Upon completion of the call, the webhook URL will receive a response like the following. In this case, the agent has three [information extractor actions](/use-the-information-extractor) to get information after the call is completed.

```json
{
    "status": "completed",
    "error_message": "",
    "lead": {
        "name": "test",
        "phone_number": "XXXXXXXX",
        "prompt_variables": {}
    },
    "call": {
        "status": "completed",
        "end_call_reason": "agent_goodbye",
        "model_id": "[UUID]",
        "timezone": "Europe/Berlin",
        "call_id": "[UUID]",
        "duration": 113,
        "start_time": "2025-10-27T10:59:46+01:00",
        "transcript": "\nbot: Hello, thank you for calling [Summit Valley College]! This is [Brian], the virtual receptionist. How can I assist you today? \nhuman: Hi. I'm\nhuman: what are you? A receptionist of what?\nbot: Hey! I'm the virtual receptionist for Summit Valley College. Basically, I help answer questions, guide you to the right department, or assist with anything related to the college. What can I help you with today? \nhuman: Yeah. Well, I am very busy right now, but\nhuman: I would like to book an appointment.\nbot: Got it! You'd like to meet with an academic adviser tomorrow. Do you have a preferred time, or should I look for the earliest available slot? \nhuman: An academic.\nhuman: An academic adviser, please, and tomorrow if it's possible.\nhuman: Look for the earliest slot, please.\nbot: Alright, I'll check for the earliest available slot with an academic adviser tomorrow. Can I get your full name and a phone number to confirm the appointment? \nhuman: My name is Julian\nhuman: and phone number is the same one I'm calling with.\nbot: Perfect, Julian! I'll use the number you're calling from. Just to confirm, you want the earliest appointment tomorrow with an academic adviser. Is there anything specific you'd like to discuss during your meeting? \nhuman: That would be all. Thank you.\nbot: No worries, Julian! If you think of anything else or need to change your appointment, just let me know. I'm here whenever you need. Have a good one! \nhuman: Same. But, like,",
        "recording_sid": null,
        "recording_url": "[REDACTED]",
        "call_sid": "",
        "agents_used": [],
        "diversion": null,
        "external_id": ""
    },
    "executed_actions": {
        "extract_info_user email": {
            "name": "extract_info_user email",
            "action_type": "extract_info_action_type",
            "call_id": "[UUID]",
            "model_id": "[UUID]",
            "workspace_id": "[UUID]",
            "description": "Get information with question: What is the user's best email for follow ups?",
            "parameters_hard_coded": {
                "identifier": "user",
                "condition": "What is the user's best email for follow ups?",
                "choices": null,
                "examples": [
                    "daniel.wasserlauf@paigo.tech",
                    "matt.sun@trychamp.co",
                    "johnSmith@gmail.com",
                    "alice.johnson13@outlook.com"
                ]
            },
            "parameters_from_llm": {},
            "error_message": null,
            "speak_on_failure": "",
            "return_value": {
                "user email": null
            },
            "is_relevant_action": true,
            "run_action_before_call_start": false,
            "timestamp": 1761559301.6653843,
            "timestamp_datetime": "2025-10-27T10:01:41.665384",
            "parameters_schema": {},
            "message_before_action": "",
            "use_hard_coded_message": true,
            "delay_message_time_seconds": 3,
            "delay_message": "",
            "action_failed_timeout": 7,
            "real_time_action": false,
            "is_interruptible": true,
            "action_id": "[UUID]"
        },
        "extract_info_zip code": {
            "name": "extract_info_zip code",
            "action_type": "extract_info_action_type",
            "call_id": "[UUID]",
            "model_id": "[UUID]",
            "workspace_id": "[UUID]",
            "description": "Get information with question: What is the user's zip code for their records?",
            "parameters_hard_coded": {
                "identifier": "zip",
                "condition": "What is the user's zip code for their records?",
                "choices": null,
                "examples": [
                    "Example 1",
                    "Example 2"
                ]
            },
            "parameters_from_llm": {},
            "error_message": null,
            "speak_on_failure": "",
            "return_value": {
                "zip code": null
            },
            "is_relevant_action": true,
            "run_action_before_call_start": false,
            "timestamp": 1761559301.4698737,
            "timestamp_datetime": "2025-10-27T10:01:41.469874",
            "parameters_schema": {},
            "message_before_action": "",
            "use_hard_coded_message": true,
            "delay_message_time_seconds": 3,
            "delay_message": "",
            "action_failed_timeout": 7,
            "real_time_action": false,
            "is_interruptible": true,
            "action_id": "[UUID]"
        },
        "extract_info_annual income": {
            "name": "extract_info_annual income",
            "action_type": "extract_info_action_type",
            "call_id": "[UUID]",
            "model_id": "[UUID]",
            "workspace_id": "[UUID]",
            "description": "Get information with question: What is the user's approximate annual income?",
            "parameters_hard_coded": {
                "identifier": "annual",
                "condition": "What is the user's approximate annual income?",
                "choices": null,
                "examples": [
                    "Example 1",
                    "Example 2"
                ]
            },
            "parameters_from_llm": {},
            "error_message": null,
            "speak_on_failure": "",
            "return_value": {
                "annual income": null
            },
            "is_relevant_action": true,
            "run_action_before_call_start": false,
            "timestamp": 1761559301.0049357,
            "timestamp_datetime": "2025-10-27T10:01:41.004936",
            "parameters_schema": {},
            "message_before_action": "",
            "use_hard_coded_message": true,
            "delay_message_time_seconds": 3,
            "delay_message": "",
            "action_failed_timeout": 7,
            "real_time_action": false,
            "is_interruptible": true,
            "action_id": "[UUID]"
        }
    },
    "analysis": {
        "persona": "true",
        "style": "true",
        "steps": "true",
        "no_repetition": "true",
        "objections": "not_applicable",
        "objection_not_defined": "not_applicable",
        "knowledge": "not_applicable",
        "goal": "partial",
        "appointment": "true",
        "user_sentiment": "partial",
        "agent_sentiment": "true",
        "call_completion": "true",
        "answered_by_human": "true",
        "opted_in": "true",
        "no_opt_out": "true",
        "call_summary": "true",
        "persona_feedback": "",
        "style_feedback": "",
        "steps_feedback": "",
        "no_repetition_feedback": "",
        "objections_feedback": "",
        "objection_not_defined_feedback": "",
        "knowledge_feedback": "",
        "goal_feedback": "User didn't specify the exact time for the appointment, but the agent proceeded with booking the earliest available slot.",
        "appointment_feedback": "",
        "user_sentiment_feedback": "The user seemed busy and did not engage much beyond booking the appointment.",
        "agent_sentiment_feedback": "",
        "call_completion_feedback": "",
        "answered_by_human_feedback": "",
        "opted_in_feedback": "",
        "no_opt_out_feedback": "",
        "call_summary_feedback": "The user called to book an appointment with an academic adviser for the earliest available slot tomorrow. The agent confirmed the details and completed the booking.",
        "judge_found_partial_or_error": true,
        "judge_found_full_error": false,
        "all_feedback": "User didn't specify the exact time for the appointment, but the agent proceeded with booking the earliest available slot. The user seemed busy and did not engage much beyond booking the appointment."
    },
    "metadata": {},
    "collected_variables": {
        "user_name": {
            "value": "Julian",
            "collected": true
        },
        "appointment_date": {
            "value": "2025-10-28",
            "collected": true
        }
    }
}
```

### Call Status options

The call object in the webhook response contains a status field which can have various values, each indicating a different state of the phone call. Below is a table explaining these statuses:

| Status Code           | Description                                                        |
| :-------------------- | :----------------------------------------------------------------- |
| pending               | Phone call is queued and will be executed.                         |
| ringing               | Phone call is ringing the recipient.                               |
| in-progress           | Phone call is currently active.                                    |
| completed             | Phone call was successful.                                         |
| no-answer             | Phone call has not been answered.                                  |
| busy                  | Recipient's line is occupied with another call.                    |
| failed                | Phone call failed.                                                 |
| canceled              | Phone call was canceled before completion.                         |
| user-canceled         | Caller hung up within the first second, before the agent answered. |
| hangup\_on\_voicemail | Voicemail was detected and the call was terminated.                |
| left\_voicemail       | Voicemail was detected and the agent left a voicemail message.     |
| spam                  | Call was flagged as spam.                                          |
| paused                | Phone call is put on hold.                                         |
| registered            | Call has been registered in the system.                            |

### End Call Reason options

This **end\_call\_reason** parameter provides insight into the reason why a call ended:

| Reason Code               | Description                                                                                                                                     |
| :------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------- |
| voicemail                 | The call was answered by voicemail.                                                                                                             |
| voicemail\_message\_left  | The agent detected voicemail and left a voicemail message.                                                                                      |
| human\_goodbye            | The call was answered by a human, and the human was the first to say goodbye.                                                                   |
| agent\_goodbye            | The call was answered by a human, and the agent was the first to say goodbye.                                                                   |
| human\_pick\_up\_cut\_off | The call was answered by a human but was cut off abruptly without a proper goodbye.                                                             |
| max\_duration             | The call exceeded the maximum set duration.                                                                                                     |
| custom\_end\_call         | The call was ended because a custom [end-call reason](/end-call-reasons) was triggered. Check `custom_end_call_reason` for the specific reason. |
| user\_canceled            | The caller hung up within the first second, before the agent answered.                                                                          |
| undefined                 | The reason for the call ending is not specified.                                                                                                |

When a call ends due to a custom end-call reason, the response also includes a `custom_end_call_reason` field with the specific condition that triggered the termination (e.g., `"Customer is being abusive"`).

### Post-Call Analytics

The `analysis` object contains automated quality assessments generated after every call. Each field evaluates a specific aspect of the conversation and returns one of the following values:

* `true`: the criterion was met
* `false`: the criterion was not met
* `partial`: the criterion was partially met (only for fields that support it)
* `not_applicable` or `null`: the criterion does not apply to this call

Every field also has a corresponding `_feedback` field (e.g. `persona_feedback`) that provides a short explanation when the result is not `true`. Feedback is empty when the criterion is fully met or not applicable.

| Field                   | Possible Values                              | Description                                                                                                                                                            |
| :---------------------- | :------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `persona`               | `true` / `false` / `not_applicable` / `null` | Whether the agent presented itself according to the persona defined in its prompt.                                                                                     |
| `style`                 | `true` / `false` / `not_applicable` / `null` | Whether the conversation tone and wording matched the style defined in the prompt (e.g. professional, casual, friendly).                                               |
| `steps`                 | `true` / `false` / `not_applicable` / `null` | Whether the agent followed the steps defined in its prompt in the correct order.                                                                                       |
| `no_repetition`         | `true` / `false`                             | Whether the agent avoided unnecessary repetition. Returns `true` if the agent did not repeat information the caller had already heard or acknowledged.                 |
| `objections`            | `true` / `false` / `not_applicable` / `null` | Whether the agent handled caller objections according to the specific instructions in the prompt. Returns `not_applicable` when no objections occurred.                |
| `objection_not_defined` | `true` / `false` / `not_applicable` / `null` | How the agent handled objections that were not covered in the prompt. Returns `not_applicable` when no such objections occurred.                                       |
| `knowledge`             | `true` / `false` / `not_applicable` / `null` | Whether the agent applied relevant domain knowledge beyond what was provided in its prompt. Returns `not_applicable` when no outside knowledge was needed.             |
| `goal`                  | `true` / `partial` / `false`                 | Whether the agent achieved the call objective defined in the prompt. Returns `partial` when the objective was only partly achieved.                                    |
| `appointment`           | `true` / `false` / `not_applicable` / `null` | Whether the date, time, and timezone were handled correctly when the caller wanted to book an appointment. Returns `not_applicable` when no appointment was requested. |
| `user_sentiment`        | `true` / `partial` / `false`                 | Whether the caller's sentiment was positive during the call.                                                                                                           |
| `agent_sentiment`       | `true` / `partial` / `false`                 | Whether the agent's sentiment was positive during the call.                                                                                                            |
| `call_completion`       | `true` / `false`                             | Whether the call ended normally with a goodbye. Returns `false` if the call was cut off mid-conversation.                                                              |
| `answered_by_human`     | `true` / `false`                             | Whether the call was answered by a human. Returns `false` if answered by voicemail or another machine, or not answered at all.                                         |
| `opted_in`              | `true` / `false`                             | Whether the caller expected the call and remembered opting in. Returns `false` for cold calls without prior opt-in.                                                    |
| `no_opt_out`            | `true` / `false`                             | Whether the caller did not request to opt out of future communication. Returns `false` if the caller asked to be removed.                                              |
| `call_summary`          | `true`                                       | Always `true`. The corresponding `call_summary_feedback` field contains a concise summary of the call (under 500 characters).                                          |

#### Aggregate Fields

| Field                          | Type      | Description                                                           |
| :----------------------------- | :-------- | :-------------------------------------------------------------------- |
| `judge_found_partial_or_error` | `boolean` | `true` if any field returned `false` or `partial`.                    |
| `judge_found_full_error`       | `boolean` | `true` if any field returned `false`.                                 |
| `all_feedback`                 | `string`  | Combined feedback from all fields that returned `partial` or `false`. |

### Collected Variables

The `collected_variables` field contains the **collected variables (slots)** from your conversation flow. This allows you to access user-provided information programmatically.

| Field                | Type           | Description                                                                                                                         |
| :------------------- | :------------- | :---------------------------------------------------------------------------------------------------------------------------------- |
| collected\_variables | object \| null | Collected variables from the conversation flow. Only variables with `collected: true` are included. May be `null` if not available. |

The object contains the variable names as keys, with each variable having:

| Field     | Type    | Description                                                                             |
| :-------- | :------ | :-------------------------------------------------------------------------------------- |
| value     | any     | The collected value of the variable (string, number, boolean, or null if not collected) |
| collected | boolean | Whether this variable was successfully collected during the conversation                |

**Example:**

```json
{
    "collected_variables": {
        "user_name": {
            "value": "Julian",
            "collected": true
        },
        "appointment_date": {
            "value": "2025-10-28",
            "collected": true
        }
    }
}
```

Only variables with `collected: true` are included in the response. System variables and variables with `collected: false` are filtered out.

## Inbound webhook

Before an inbound call connects to an agent, Synthflow sends a `call_inbound` event to your endpoint so you can reroute the call or inject context.

<iframe src="https://player.vimeo.com/video/1101493486?h=7165ddc192&badge=0&autopause=0&player_id=0&app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media; web-share" title="Inbound Webhook Synthflow" />

You can intercept, reroute and decline every incoming call with inbound call webhooks.

Set `inbound_call_webhook_url` on the agent to your receiver endpoint when [creating](/api-reference/platform-api/agents/create-assistant) or [updating](/api-reference/platform-api/agents/update-assistant) an assistant.

You will receive a JSON payload (`event: "call_inbound"`) within 10 seconds after a call was initiated. Your endpoint will need to respond with an updated `call_inbound` object to assign an agent to the call or update the metadata.

### What to expect

* If your endpoint responds with an empty call\_inbound object (e.g. `{}` with no agent assignment), the call will be disconnected.
* If you return `override_model_id` as an empty string (`""`), the call continues with the agent that received the call (no reroute, no disconnect).
* If your endpoint doesn't respond within timeout, the call is put on hold. Up to 3 more requests will be sent. If all these attempts fail, the call will be rerouted to the `default_agent` you specified. If you did not specify a `default_agent`, the call will be disconnected.

### Requests

Every inbound call will generate and send a JSON in this format:

```json
{
  "event": "call_inbound",
  "call_inbound": {
    "model_id": "optional_agent_id", // the default agent, if preconfigured by you
    "from_number": "+12137771234", // the caller
    "to_number": "+12137771235" // the receiving number
  }
}
```

### Responses

Your endpoint needs to respond with an updated call\_inbound object.

```json
{
  "call_inbound": {
    "override_model_id": "agent_67890", // reroute to another agent; use "" to keep the default agent
    "custom_variables": { // context and data for further call flow
      "customer_name": "John Doe" 
    },
    "metadata": { // metadata for post-call analysis
      "session_id": "abc123"
    }
  }
}
```

## FAQ

Synthflow does not enforce a fixed maximum payload size for post-call webhooks. In most payload size incidents, the receiving endpoint rejects the request because of its own request body limit.

Yes. Your receiving system can return HTTP `413 Payload Too Large` when the webhook body exceeds its configured limit. This is common when calls are long, transcripts are large, or many actions are included in one payload.

Start by checking your endpoint logs for HTTP `413` responses. Then increase the accepted request body size in your webhook receiver and any proxy in front of it, such as Cloudflare, API Gateway, Nginx, or your app server. If needed, reduce payload size by disabling transcript storage downstream or trimming what you persist.

Check your endpoint logs first, including HTTP `413` for oversized payloads. Then open **Logs > Webhook logs** in the dashboard to see the request Synthflow sent and the response your server returned. See [Webhook logs](/logs#webhook-logs). You can also query deliveries via the [webhook logs API](/api-reference/platform-api/webhook-logs/list-webhook-logs).