> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brilo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Post-Call Webhook

> Receive call data at your CRM as soon as a Brilo agent finishes a call

## Overview

Brilo sends an HTTPS `POST` to your endpoint after every call your agent handles. The body is a single JSON object containing everything you need to record the call in your CRM: who called whom, what was said, how long it lasted, the agent's summary, and any structured data your agent extracted during the conversation.

One call produces one delivery per webhook target. If delivery fails, Brilo retries automatically (see [Delivery and retries](#delivery-and-retries)).

<Note>
  This page describes payload version **1.1**. The `payload_version` field at the top of the body tells you which version you're parsing — when we add fields in a future version, older receivers keep working.
</Note>

***

## Configuring a webhook

Each agent can have its own webhook URL. Add it from the agent builder:

1. Open your agent in the [Brilo dashboard](https://dashboard.brilo.ai).
2. Go to the **Actions** section of the agent configuration.
3. Add a **Webhook** action and paste your endpoint URL.
4. Optionally add custom request headers (e.g. an `Authorization` header with a shared secret) — Brilo passes these through on every delivery.
5. Save the agent.

<Tip>
  **Shared-secret authentication.** Brilo does not currently sign webhook bodies. To verify deliveries came from your own Brilo account, set a custom header like `Authorization: Bearer <your-secret>` on the webhook action and check it on your endpoint.
</Tip>

The same agent can have multiple webhook actions configured — each one receives an independent `POST` per call.

***

## When the webhook fires

Brilo sends the webhook **after** the call ends *and* after the agent's post-call analysis has completed (summary generation, success-check evaluation, and structured-data extraction). This means transcripts and analysis fields are populated before delivery starts.

The webhook fires for calls in any terminal status — including `completed`, `failed`, and `blocked`. For calls that ended before any conversation took place, the transcript and summary will be sparse but the envelope is otherwise identical.

***

## Request basics

|                         |                                                |
| ----------------------- | ---------------------------------------------- |
| **Method**              | `POST`                                         |
| **Content-Type**        | `application/json`                             |
| **Body**                | One JSON object (described below)              |
| **Expected response**   | Any `2xx` status code, within 10 seconds       |
| **Per-call deliveries** | One per webhook action configured on the agent |

***

## Body schema

The top level of the body is a flat JSON object with up to 16 fields. The next section gives a complete realistic example; the table below documents each field individually.

| Field                       | Type                                                          | Always present?     | Description                                                                                                                                                                                                                                                                                                                                                                                   |
| --------------------------- | ------------------------------------------------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `payload_version`           | string                                                        | yes                 | Schema version. Currently `"1.1"`.                                                                                                                                                                                                                                                                                                                                                            |
| `call_id`                   | string (uuid)                                                 | yes                 | Brilo's unique identifier for this call. Use this as the idempotency key in your CRM.                                                                                                                                                                                                                                                                                                         |
| `direction`                 | string (enum)                                                 | yes                 | `"inbound"` or `"outbound"`. See [Possible values](#possible-values-and-sub-shapes).                                                                                                                                                                                                                                                                                                          |
| `call_status`               | string (enum)                                                 | yes                 | Terminal status of the call. See [Possible values](#possible-values-and-sub-shapes).                                                                                                                                                                                                                                                                                                          |
| `transcript`                | string                                                        | yes                 | The full conversation as a single human-readable string, with one `role: content` line per turn. Function-call entries are excluded from this field. Falls back to `"No transcript available"` if nothing was said.                                                                                                                                                                           |
| `transcript_object`         | array of [TranscriptEntry](#transcriptentry)                  | yes (may be empty)  | The structured transcript: every turn including agent and user utterances and any function calls the agent made.                                                                                                                                                                                                                                                                              |
| `duration`                  | integer                                                       | yes (may be `null`) | Call duration in seconds.                                                                                                                                                                                                                                                                                                                                                                     |
| `call_from`                 | string                                                        | yes (may be `null`) | The originating number. For inbound calls this is the caller; for outbound calls this is your Brilo number. E.164 format when present.                                                                                                                                                                                                                                                        |
| `call_to`                   | string                                                        | yes (may be `null`) | The destination number. For inbound calls this is your Brilo number; for outbound calls this is the contact's number. E.164 format when present.                                                                                                                                                                                                                                              |
| `agent_id`                  | string (uuid)                                                 | yes (may be `null`) | Brilo's unique identifier for the agent that handled the call.                                                                                                                                                                                                                                                                                                                                |
| `summary`                   | string                                                        | yes                 | The agent's natural-language summary of the call. Falls back to `"No summary available"` if generation failed or the call was too short.                                                                                                                                                                                                                                                      |
| `contacts`                  | array of [Contact](#contact)                                  | yes (may be empty)  | Matching contacts from your Brilo workspace. Brilo looks up contacts by the phone number on the other side of the call. Empty array if no contact matches.                                                                                                                                                                                                                                    |
| `variables`                 | object (string → string)                                      | only when supplied  | Echo of the per-call `variables` you passed when placing the call via [`POST /v1/call`](/api-reference/endpoint/call). Use this to tie the webhook back to the originating record in your CRM — e.g. a Salesforce `lead_id`. The field is **omitted entirely** (not `{}`, not `null`) when no variables were supplied on this call, including all inbound calls. See [Variables](#variables). |
| `personalized_call_summary` | [PersonalizedCallSummary](#personalizedcallsummary) or `null` | yes                 | A custom summary produced by a **Summary** action configured on the agent. `null` when no Summary action is configured or it didn't run.                                                                                                                                                                                                                                                      |
| `call_tags`                 | array of [CallTag](#calltag)                                  | yes (may be empty)  | Outcomes from **Success Check** actions configured on the agent. Only matched tags are included. Empty array if the agent has no success-check actions or none matched.                                                                                                                                                                                                                       |
| `extracted_fields`          | array of [ExtractedField](#extractedfield)                    | yes (may be empty)  | Values pulled from the conversation by **Extract Data** actions configured on the agent. One entry per configured extractor. Empty array if the agent has no extract-data actions.                                                                                                                                                                                                            |

***

## Example payload

A complete payload from a successful outbound sales call. Your endpoint will receive a body of exactly this shape.

```json theme={null}
{
  "payload_version": "1.1",
  "call_id": "8f3e8c2a-1d4f-4b7a-9c9a-2c7b1a9f8e4d",
  "direction": "outbound",
  "call_status": "completed",
  "transcript": "agent: Hi Sarah, this is Casey from Acme Roofing. Is now a good time to talk about your inspection?\nuser: Yeah, go ahead.\nagent: Great. We have an opening on Tuesday at 3pm. Does that work for you?\nuser: Tuesday at 3 works. Thanks.\nagent: Perfect, you're booked. We'll text a reminder. Have a good one!\n",
  "transcript_object": [
    {
      "role": "agent",
      "content": "Hi Sarah, this is Casey from Acme Roofing. Is now a good time to talk about your inspection?"
    },
    {
      "role": "user",
      "content": "Yeah, go ahead."
    },
    {
      "role": "agent",
      "content": "Great. We have an opening on Tuesday at 3pm. Does that work for you?"
    },
    {
      "role": "user",
      "content": "Tuesday at 3 works. Thanks."
    },
    {
      "role": "function_call",
      "content": "book_appointment(date='2026-06-09', time='15:00', name='Sarah Lin')"
    },
    {
      "role": "function_result",
      "content": "{\"booking_id\":\"appt_2034\",\"confirmed\":true}"
    },
    {
      "role": "agent",
      "content": "Perfect, you're booked. We'll text a reminder. Have a good one!"
    }
  ],
  "duration": 47,
  "call_from": "+14155550199",
  "call_to": "+14155550114",
  "agent_id": "a8b22e6d-4c93-44b1-9e44-2b1f2c3d4e5f",
  "summary": "Casey reached out to Sarah to schedule a roof inspection. Sarah agreed to Tuesday at 3pm. Booking was created and a reminder text was promised. Call ended on a positive note.",
  "contacts": [
    {
      "id": "c1f2d3e4-5a6b-7c8d-9e0f-1a2b3c4d5e6f",
      "email": "sarah.lin@example.com",
      "phone": "+14155550114",
      "first_name": "Sarah",
      "last_name": "Lin",
      "custom_fields": {
        "salesforce_lead_id": "00Q5e000001abcd",
        "lead_owner_name": "Casey Jones",
        "company": "Lin Residence"
      }
    }
  ],
  "variables": {
    "lead_id": "00Q5e000001abcd",
    "lead_name": "Sarah",
    "product": "Roof inspection",
    "appt_time": "Tuesday 3pm"
  },
  "personalized_call_summary": {
    "action_id": "11111111-2222-3333-4444-555555555555",
    "name": "Sales Recap",
    "content": "Booked Sarah for a roof inspection Tuesday 3pm. Reminder text on file. No pricing objections raised."
  },
  "call_tags": [
    {
      "action_id": "aaaa1111-bbbb-2222-cccc-333333333333",
      "key": "appointment_booked",
      "name": "Appointment Booked",
      "icon": "calendar-check",
      "matched": true
    }
  ],
  "extracted_fields": [
    {
      "action_id": "bbbb2222-cccc-3333-dddd-444444444444",
      "key": "appointment_date",
      "name": "Appointment date",
      "format": "text",
      "value": "Tuesday 3pm"
    },
    {
      "action_id": "cccc3333-dddd-4444-eeee-555555555555",
      "key": "confirmed_attendance",
      "name": "Confirmed attendance",
      "format": "boolean",
      "value": true
    }
  ]
}
```

***

## Possible values and sub-shapes

### `direction`

| Value        | Meaning                            |
| ------------ | ---------------------------------- |
| `"inbound"`  | A caller dialed your Brilo number. |
| `"outbound"` | Your agent dialed a contact.       |

### `call_status`

The status of the call at the moment the webhook fires. The webhook is only sent for terminal states.

| Value           | Meaning                                                                                                                |
| --------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `"completed"`   | The call connected and ended normally. The most common status.                                                         |
| `"failed"`      | The call could not connect, or terminated abnormally (carrier error, network issue, etc.).                             |
| `"blocked"`     | The call was blocked before it could be placed — typically a Do-Not-Call list match or a workspace-level policy block. |
| `"in-progress"` | Rare: only seen if a webhook is retried for a call that re-entered an active state.                                    |

Non-terminal statuses (`scheduled`, `not-scheduled`, `paused`, `queued`, `dialing`) exist in the call lifecycle but never appear in webhook bodies — Brilo waits for the call to reach a terminal state before delivering.

### TranscriptEntry

Each item in `transcript_object` is an object with at least `role` and `content`. Additional keys may be present depending on the LLM provider — these are forwarded as-is and are safe to ignore.

| Field     | Type   | Description                                                                                |
| --------- | ------ | ------------------------------------------------------------------------------------------ |
| `role`    | string | One of `"agent"`, `"user"`, `"function_call"`, `"function_result"`.                        |
| `content` | string | The utterance text, or for function calls a printable representation of the call / result. |

| `role`              | What it represents                                                               |
| ------------------- | -------------------------------------------------------------------------------- |
| `"agent"`           | Something the Brilo agent said.                                                  |
| `"user"`            | Something the human caller said.                                                 |
| `"function_call"`   | The agent invoked a tool/function during the call. `content` describes the call. |
| `"function_result"` | The tool returned a result. `content` is the result, usually a JSON string.      |

<Note>
  The flattened `transcript` string at the top of the body **excludes** `function_call` and `function_result` entries (so it reads like a clean human conversation). The `transcript_object` array **includes** them. If you want a complete audit trail, use `transcript_object`.
</Note>

### Contact

A row from your Brilo workspace's `contacts` table, matched on the phone number on the other side of the call (the contact's number, not your Brilo number).

| Field           | Type                  | Description                                                                                                                                                                                                |
| --------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`            | string (uuid)         | Brilo's contact ID.                                                                                                                                                                                        |
| `email`         | string or `null`      | Contact email if known.                                                                                                                                                                                    |
| `phone`         | string                | Phone number in E.164.                                                                                                                                                                                     |
| `first_name`    | string or `null`      |                                                                                                                                                                                                            |
| `last_name`     | string or `null`      |                                                                                                                                                                                                            |
| `custom_fields` | object (string → any) | Always present (defaults to `{}`). The per-contact key/values attached to this contact when it was added to Brilo — for example the CRM record ID it originated from. See [Custom fields](#custom-fields). |

If multiple contacts share the same phone in the workspace, all matches are included. If no contact matches, `contacts` is `[]`.

#### Custom fields

`contacts[].custom_fields` echoes back, **verbatim**, whatever key/values were stored on the contact — the columns from a CSV import, or the fields set when the contact was added to a campaign. This is where a CRM record ID lives when it belongs to the **contact** rather than to a single call.

```json theme={null}
"custom_fields": {
  "salesforce_lead_id": "00Q5e000001abcd",
  "lead_owner_name": "Casey Jones",
  "company": "Lin Residence"
}
```

| Aspect   | Behavior                                                                                                                                                                         |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Presence | Always present on every contact. Defaults to `{}` when the contact has no stored fields.                                                                                         |
| Keys     | Exactly the keys stored on the contact. **Not** restricted to the workspace's defined `custom_field` set, and not renamed or reordered — whatever was imported comes back as-is. |
| Values   | Returned as stored. Usually strings, but any JSON value the import produced is passed through unchanged.                                                                         |

<Warning>
  **`custom_fields` vs `variables` — pick the right one for CRM tie-back.** Both can carry a record ID, and they come from different places:

  * **`contacts[].custom_fields`** — attached to the **contact** (CSV / campaign import). Use this when your leads were uploaded with their CRM IDs. To read a Salesforce Lead ID, map `contacts[0].custom_fields.salesforce_lead_id` (substitute your own key).
  * **`variables`** — supplied **per call** on [`POST /v1/call`](/api-reference/endpoint/call). Use this only when you place calls programmatically and pass the ID at call time.

  If your contacts were imported from a CSV or campaign, the ID is in `custom_fields`, **not** `variables` — a receiver that only reads `variables` will find nothing and silently skip the record even though the ID was delivered.
</Warning>

### Variables

Round-trips the per-call `variables` object you supplied when placing the call via [`POST /v1/call`](/api-reference/endpoint/call). Brilo persists the values on the call record and echoes them back here so you can correlate the webhook event with the originating record in your CRM — typically a Salesforce `lead_id`, a HubSpot record ID, or your own customer ID.

| Aspect       | Behavior                                                                                                                                                                                                                                         |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Shape        | Flat object of string keys → string values. Values you originally sent as numbers or booleans are coerced to strings (`true` → `"true"`, `42` → `"42"`).                                                                                         |
| When present | Only on outbound calls placed with a non-empty `variables` body.                                                                                                                                                                                 |
| When absent  | The key is **omitted from the payload entirely** — not `null`, not `{}`. This includes all inbound calls and outbound calls placed without `variables`. Use `if ("variables" in body)` or `body.get("variables")` rather than truthiness checks. |
| Constraints  | Identical to the request: max 50 keys, keys match `^[a-zA-Z][a-zA-Z0-9_]{0,62}$` (case-insensitive), each value ≤ 1000 characters, whole object ≤ 4KB serialized.                                                                                |
| Stability    | Whatever keys you sent come back unchanged. Brilo does not add, rename, or reorder keys.                                                                                                                                                         |

```json theme={null}
"variables": {
  "lead_id": "00Q5e000001abcd",
  "campaign_source": "google_ads",
  "priority": "high"
}
```

<Tip>
  **CRM tie-back pattern.** Include your record ID (`lead_id`, `contact_id`, `ticket_id`, etc.) in `variables` when calling [`POST /v1/call`](/api-reference/endpoint/call). In your webhook receiver, read `body.variables[YOUR_KEY]` and use it to find the matching CRM row. The `call_id` Brilo returns stays useful as a per-call idempotency key on top of that.
</Tip>

### PersonalizedCallSummary

Output of a **Summary** action (`metadata.type = "call_summary"`) configured on the agent. Use this when you want a domain-specific summary (e.g. "Sales Recap", "Support Triage Notes") in addition to the generic `summary` field.

| Field       | Type          | Description                                       |
| ----------- | ------------- | ------------------------------------------------- |
| `action_id` | string (uuid) | The configured action that produced this summary. |
| `name`      | string        | The action's display name (e.g. `"Sales Recap"`). |
| `content`   | string        | The generated summary text.                       |

The whole object is `null` if no Summary action is configured on the agent or if it failed to run.

### CallTag

Output of a **Success Check** action (`metadata.type = "success_check"`) — a yes/no criterion the agent evaluates against the conversation. Only **matched** success checks appear in `call_tags`.

| Field       | Type             | Description                                                                                                                                                  |
| ----------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `action_id` | string (uuid)    | The configured action.                                                                                                                                       |
| `key`       | string           | A slugified, machine-readable key derived from `name` (e.g. `"Appointment Booked"` → `"appointment_booked"`). Stable for the same action; safe to switch on. |
| `name`      | string           | The action's display name.                                                                                                                                   |
| `icon`      | string or `null` | Optional icon identifier set on the action.                                                                                                                  |
| `matched`   | boolean          | Always `true` in the body (only matched tags are emitted). The field is included for future-compatibility.                                                   |

### ExtractedField

Output of an **Extract Data** action (`metadata.type = "extract_data"`) — a value the agent pulls out of the conversation (appointment date, budget, lead score, etc.). One entry per configured extractor on the agent, regardless of whether extraction found a value.

| Field       | Type          | Description                                                                                                                                                                                  |
| ----------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `action_id` | string (uuid) | The configured action.                                                                                                                                                                       |
| `key`       | string        | Slugified key derived from `name`. Safe to use as a column name in your CRM.                                                                                                                 |
| `name`      | string        | The action's display name.                                                                                                                                                                   |
| `format`    | string        | The declared output format: `"text"`, `"boolean"`, `"number"`, etc.                                                                                                                          |
| `value`     | varies        | The extracted value, cast to the declared format. `null` if extraction found nothing. When `format` is `"boolean"`, the value is `true` or `false`. For other formats the value is a string. |

<Tip>
  Use `key` (not `name`) as your CRM column identifier. `name` is user-editable and may change; `key` is stable as long as the action's name doesn't change drastically.
</Tip>

***

## Receiving the webhook

A minimal receiver that parses the body, acknowledges quickly, and queues heavy work for after the response is sent.

<CodeGroup>
  ```javascript Node (Express) theme={null}
  import express from "express";

  const app = express();
  app.use(express.json({ limit: "1mb" }));

  app.post("/brilo/webhook", (req, res) => {
    // Optional: verify a shared-secret header you set on the webhook action.
    if (req.headers.authorization !== `Bearer ${process.env.BRILO_WEBHOOK_SECRET}`) {
      return res.status(401).send("unauthorized");
    }

    const body = req.body;
    // Acknowledge fast — within 10 seconds, ideally under 1.
    res.status(200).send("ok");

    // Then process. Use call_id as the idempotency key in your CRM.
    // If you passed a Salesforce lead_id (or any other CRM key) on the
    // originating /v1/call request, it comes back on body.variables.
    queueForCrmUpsert({
      callId: body.call_id,
      leadId: body.variables?.lead_id, // tie-back to Salesforce (or other CRM)
      direction: body.direction,
      status: body.call_status,
      duration: body.duration,
      summary: body.summary,
      customSummary: body.personalized_call_summary?.content,
      tags: body.call_tags.map(t => t.key),
      fields: Object.fromEntries(body.extracted_fields.map(f => [f.key, f.value])),
      contact: body.contacts[0] ?? null,
      transcript: body.transcript_object,
    });
  });

  app.listen(3000);
  ```

  ```python Python (FastAPI) theme={null}
  from fastapi import FastAPI, Header, HTTPException, Request
  import os

  app = FastAPI()
  SECRET = os.environ["BRILO_WEBHOOK_SECRET"]

  @app.post("/brilo/webhook")
  async def brilo_webhook(request: Request, authorization: str | None = Header(default=None)):
      # Optional: verify a shared-secret header you set on the webhook action.
      if authorization != f"Bearer {SECRET}":
          raise HTTPException(status_code=401, detail="unauthorized")

      body = await request.json()

      # Use call_id as the idempotency key in your CRM.
      # If you passed a Salesforce lead_id (or any other CRM key) on the
      # originating /v1/call request, it comes back on body["variables"].
      variables = body.get("variables") or {}
      queue_for_crm_upsert(
          call_id=body["call_id"],
          lead_id=variables.get("lead_id"),  # tie-back to Salesforce (or other CRM)
          direction=body["direction"],
          status=body["call_status"],
          duration=body["duration"],
          summary=body["summary"],
          custom_summary=(body.get("personalized_call_summary") or {}).get("content"),
          tags=[t["key"] for t in body["call_tags"]],
          fields={f["key"]: f["value"] for f in body["extracted_fields"]},
          contact=(body["contacts"] or [None])[0],
          transcript=body["transcript_object"],
      )

      # Acknowledge after enqueueing — keep total time under 10s.
      return {"status": "ok"}
  ```
</CodeGroup>

<Warning>
  Respond with `2xx` to acknowledge receipt — your CRM write should happen **after** the response, not before. Anything non-`2xx` (including `4xx` you might return for "invalid data") will trigger Brilo's retry policy. To reject a delivery permanently, log the issue and return `200` anyway.
</Warning>

***

## Delivery and retries

Brilo delivers webhooks through a Temporal workflow with strict retry semantics.

|                           |                                                                                                                 |
| ------------------------- | --------------------------------------------------------------------------------------------------------------- |
| **Transport**             | HTTPS `POST` with `Content-Type: application/json`                                                              |
| **HTTP request timeout**  | 10 seconds per attempt                                                                                          |
| **Maximum attempts**      | 5 (the first attempt plus up to 4 retries)                                                                      |
| **Backoff**               | Exponential, starting at 5 seconds, doubling each retry                                                         |
| **Retry schedule**        | Attempts at approximately t=0s, +5s, +15s, +35s, +75s                                                           |
| **Retry triggers**        | Any non-`2xx` HTTP response, network error, or timeout                                                          |
| **Idempotency**           | A delivery is recorded per `(call_id, webhook_action_id)`. Once a target has received a `2xx`, retries skip it. |
| **Total delivery window** | The 5 attempts span roughly 75 seconds of backoff from the first attempt                                        |

If all 5 attempts fail, the delivery is abandoned. Brilo does not currently expose a UI for listing failed deliveries — if you need to recover missed calls, you can fetch them by ID using the [Get Call](/api-reference/endpoint/call) endpoint.

<Tip>
  **Make your receiver idempotent.** Brilo's idempotency only protects against duplicates from *our* retry path. If your endpoint returns `2xx` but your downstream CRM write fails, you'll want to use `call_id` as a unique key so a manual replay doesn't double-write.
</Tip>

***

## Edge cases & FAQ

<AccordionGroup>
  <Accordion title="Why is `contacts` empty?">
    Brilo looks up contacts by the phone number on the other side of the call (the caller for inbound, the dialled number for outbound) within the same workspace as the call. If no contact in your workspace matches that number, `contacts` is `[]`. Add the contact in Brilo and future calls will populate the array.
  </Accordion>

  <Accordion title="Why does `summary` say 'No summary available'?">
    This fallback is used when the agent's post-call summary generation produced no usable output — usually because the call ended before any meaningful conversation took place, or because the LLM returned an empty response. The webhook is still delivered with the rest of the data.
  </Accordion>

  <Accordion title="Why does `transcript` say 'No transcript available'?">
    The call ended before any utterance was captured (e.g. the recipient hung up before speaking, or the call failed to connect). The `transcript_object` array will also be empty in this case.
  </Accordion>

  <Accordion title="When are `call_tags` or `extracted_fields` empty?">
    Both default to `[]`. `call_tags` is empty when no Success Check actions are configured on the agent or none matched. `extracted_fields` is empty when no Extract Data actions are configured on the agent. If actions are configured but the conversation didn't surface a value, individual entries will still appear with `value: null`.
  </Accordion>

  <Accordion title="Can I receive multiple webhooks for the same call?">
    Yes, if you've configured multiple webhook actions on the same agent. Each webhook action receives its own delivery for every call. Brilo's internal idempotency prevents duplicate deliveries to the *same* target — but two different targets will both fire.
  </Accordion>

  <Accordion title="What happens if my endpoint is slow?">
    Brilo waits up to 10 seconds for a response. Slower responses are treated as a timeout and trigger a retry. Make your endpoint respond fast (acknowledge first, process async).
  </Accordion>

  <Accordion title="What if my endpoint returns a 4xx?">
    Brilo treats any non-`2xx` as a retryable failure and will attempt delivery up to 5 times total (the first attempt plus up to 4 retries). If your endpoint sees an "invalid" payload it can't process, log it and return `200` to stop the retries.
  </Accordion>

  <Accordion title="Will the schema change?">
    We add fields additively — your receiver should ignore fields it doesn't know about. When the schema gains new fields, `payload_version` will increment (e.g. `"1.2"`). We won't remove fields or change types within a major version. See the [Changelog](#changelog).
  </Accordion>

  <Accordion title="How do I correlate the webhook back to my CRM record (Salesforce, HubSpot, etc.)?">
    Pass your CRM record ID as a `variables` entry when placing the call:

    ```json theme={null}
    POST /v1/call
    {
      "to": "+14155550114",
      "agent_id": "...",
      "variables": { "lead_id": "00Q5e000001abcd" }
    }
    ```

    The same `variables` object is echoed back at the top level of the post-call webhook body, so your receiver can look up the originating record without first having to join through `call_id`. The field is **omitted entirely** when no variables were sent — guard with `body.variables?.lead_id` (JS) or `(body.get("variables") or {}).get("lead_id")` (Python). See [Variables](#variables) for the full contract.
  </Accordion>

  <Accordion title="How do I test my webhook receiver without making real calls?">
    Place a real test call to your agent. Brilo doesn't currently send synthetic test deliveries — every webhook corresponds to a real call. We recommend keeping a dedicated test agent with a separate webhook URL pointing at a staging endpoint.
  </Accordion>
</AccordionGroup>

***

## Changelog

### v1.1 (additive update — `variables` echo)

Adds an additive top-level field for CRM tie-back:

* `variables` — echo of the per-call `variables` object supplied on `POST /v1/call`. Omitted from the payload when no variables were sent on the originating call, so receivers pinned to the earlier v1.1 shape keep working unchanged.

### v1.1

Adds three top-level analysis sections derived from the agent's post-call actions:

* `personalized_call_summary` — output of any **Summary** action configured on the agent
* `call_tags` — matched **Success Check** outcomes
* `extracted_fields` — **Extract Data** values

Adds a `payload_version` field at the top of the body. Delivery now uses a Temporal-backed retry workflow: 5 attempts with exponential backoff, per-target idempotency.

### v1.0

Original payload: `call_id`, `direction`, `call_status`, `transcript`, `transcript_object`, `duration`, `call_from`, `call_to`, `agent_id`, `summary`, `contacts`. Single-attempt, fire-and-forget delivery.
