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

# Scoping & Event Filtering

> Route events per agent, subscribe to specific event types, and control account-wide fan-out

# Scoping & Event Filtering

By default every webhook endpoint you configure is **account-wide**: it receives
events from every agent on your account. Two controls narrow that down.

* **Scope** — bind an endpoint to a single agent, so it receives only that
  agent's events.
* **Event allowlist** — subscribe an endpoint to specific event types, so
  everything else is filtered out.

They are independent, and they compose.

## Account vs agent scope

An endpoint created without `agent_id` is account-wide. Pass `agent_id` at
creation to bind it to one agent:

```bash theme={null}
curl -X POST https://api.vocobase.com/api/v2/config/webhooks \
  -H "Authorization: Bearer rg_live_abc123def456ghi789jkl012" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "acme-tenant",
    "url": "https://acme.example.com/vocobase",
    "agent_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }'
```

`POST /api/v2/agent/{agent_id}/webhooks` does the same thing with the scope taken
from the path; any `agent_id` in the body is ignored there. Edit, delete, rotate,
test, and delivery history stay on `/config/webhooks/{id}` for both — an endpoint
id is already unambiguous.

<Warning>
  **`agent_id` is set once and cannot be changed.** `PATCH` rejects it with
  `400 VALIDATION_ERROR`, even when the value matches the endpoint's current
  scope. Re-pointing a live endpoint at a different agent would silently change
  what an already-running integration receives, with nothing on the receiving
  end to signal it. Delete the endpoint and create a new one instead.
</Warning>

An `agent_id` that is not one of your own agents — or belongs to a deleted agent
— is rejected with `404 NOT_FOUND`, not `403`, so this route cannot be used to
probe whether an agent id exists on someone else's account.

### Limits are per scope; labels are per account

These two are counted differently, and it is easy to assume otherwise.

|                      | Counted per                                                                                                                                                                  |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The 5-endpoint limit | **Scope.** The account bucket gets its own 5, and so does every agent — a busy account is never starved by one agent's endpoints. Exceeding it returns `409 LIMIT_EXCEEDED`. |
| Label uniqueness     | **Account.** One label names exactly one endpoint across your whole account, so a delivery row is never ambiguous.                                                           |

<Warning>
  **You cannot label each agent's endpoint `prod`.** The uniqueness constraint
  is on `(account, label)`, not `(scope, label)`, so the second agent to claim
  `prod` gets `409 LABEL_TAKEN`.

  Qualify agent-scoped labels instead — `prod-acme`, `prod-globex` — or name
  them after the tenant rather than the environment.
</Warning>

<Note>
  **Deleting an agent does not delete its endpoints.** `DELETE /api/v2/agent/{id}`
  is a soft delete, so the agent's endpoint rows survive it. They keep holding
  their labels and keep counting against that agent's 5, while
  `GET`/`PATCH /agent/{id}/webhooks*` start returning `404 NOT_FOUND` because the
  agent is gone.

  Delete the endpoints yourself with `DELETE /config/webhooks/{id}` — that route
  works on an endpoint whose agent has been deleted — before you retire an agent,
  or you will strand labels you cannot reuse.
</Note>

### Agent-scoped endpoints are additive

An agent-scoped endpoint does not *replace* your account-wide ones. It adds to
them. Given:

| Endpoint      | Scope             |
| ------------- | ----------------- |
| `prod`        | Account           |
| `acme-tenant` | Agent `a1b2c3d4…` |

…a `session.completed` on agent `a1b2c3d4…` produces **two independent
deliveries** — one to `prod`, one to `acme-tenant`. Each has its own signature,
its own retry schedule, and its own row in delivery history. One failing does
not affect the other.

An event on any *other* agent reaches only `prod`.

## Turning off account-wide fan-out

Additive is the right default, but not always what you want. An agent that feeds
a different downstream system — a white-label tenant, a client migrating from
another platform, a one-off integration — should be able to stop duplicating its
traffic into your main integration.

`webhooks_include_account_endpoints` is a per-agent flag, `true` by default:

```bash theme={null}
curl -X PATCH https://api.vocobase.com/api/v2/agent/a1b2c3d4-e5f6-7890-abcd-ef1234567890/webhooks/settings \
  -H "Authorization: Bearer rg_live_abc123def456ghi789jkl012" \
  -H "Content-Type: application/json" \
  -d '{ "include_account_endpoints": false }'
```

| `include_account_endpoints` | Where this agent's events go                                  |
| --------------------------- | ------------------------------------------------------------- |
| `true` (default)            | The agent's own endpoints **and** every account-wide endpoint |
| `false`                     | The agent's own endpoints **only**                            |

The response echoes the stored value:

```json theme={null}
{
  "success": true,
  "data": { "include_account_endpoints": false }
}
```

Read the current value along with the agent's endpoints:

```bash theme={null}
curl https://api.vocobase.com/api/v2/agent/a1b2c3d4-e5f6-7890-abcd-ef1234567890/webhooks \
  -H "Authorization: Bearer rg_live_abc123def456ghi789jkl012"
```

```json theme={null}
{
  "success": true,
  "data": {
    "endpoints": [
      {
        "id": "e1f2a3b4-5678-90ab-cdef-1234567890ab",
        "label": "acme-tenant",
        "url": "https://acme.example.com/vocobase",
        "enabled": true,
        "agent_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "agent_name": "Acme Sales Bot",
        "events": ["session.completed"],
        "last_delivery_at": "2026-05-25T10:30:00.000Z",
        "last_delivery_status": 200,
        "created_at": "2026-05-25T10:00:00.000Z",
        "updated_at": "2026-05-25T10:30:00.000Z"
      }
    ],
    "include_account_endpoints": false,
    "available_events": [
      "session.completed",
      "call.status.updated",
      "call.transferred",
      "whatsapp.message.sent",
      "whatsapp.message.failed",
      "whatsapp.message.status_updated"
    ]
  }
}
```

`endpoints` lists **only** this agent's own endpoints — deliberately, so you can
see which rows actually live on the agent. It does not fold in the account-wide
ones even when `include_account_endpoints` is `true`. `available_events` is the
live event catalog; read it from here rather than hard-coding the list.

<Note>
  The flag only applies to events that carry an agent. An event with no agent
  attached always goes to your account-wide endpoints — honouring a stray
  `false` there would disable webhooks account-wide.
</Note>

### The guard: `409 NO_AGENT_ENDPOINT`

Setting the flag to `false` is refused while the agent has no **enabled**
endpoint of its own:

```json theme={null}
{
  "success": false,
  "error": {
    "code": "NO_AGENT_ENDPOINT",
    "message": "Add an enabled webhook endpoint for this agent before routing its events away from the account endpoints — otherwise they would go nowhere."
  }
}
```

Create the agent's endpoint first, then turn the flag off.

The same protection runs in reverse: if you later delete or disable the agent's
last enabled endpoint, the flag **automatically flips back to `true`** rather
than leaving the agent with nowhere to deliver. The bias is deliberate — an
unexpected duplicate is an annoyance you can dedupe, a dropped event is
unrecoverable.

## Event allowlist

`events` is a per-endpoint allowlist. Omit it, or send `[]`, and the endpoint
receives **every** event type — including ones added to the platform later.

```bash theme={null}
curl -X POST https://api.vocobase.com/api/v2/config/webhooks \
  -H "Authorization: Bearer rg_live_abc123def456ghi789jkl012" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "billing",
    "url": "https://your-server.com/vocobase/billing",
    "events": ["session.completed", "call.transferred"]
  }'
```

An unknown event name is rejected with `400 VALIDATION_ERROR` listing the valid
set, so a typo fails loudly at configuration time rather than silently
delivering nothing. Duplicates are de-duplicated rather than rejected.

### Event catalog

Six event types, and this is the complete list.

| Event                             | Fired when                                                                                                    |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `session.completed`               | A session finished and the post-call pipeline is done — transcript, recording, credits, variables, extraction |
| `call.status.updated`             | Telephony call status changed (ringing, answered, terminal disposition)                                       |
| `call.transferred`                | The agent handed the call to a human or another number                                                        |
| `whatsapp.message.sent`           | An outbound WhatsApp message was accepted by the provider                                                     |
| `whatsapp.message.failed`         | An outbound WhatsApp message could not be sent                                                                |
| `whatsapp.message.status_updated` | Delivery or read receipt for a WhatsApp message                                                               |

<Warning>
  There is **no** `session.failed` event. A session that ends in a terminal
  failure arrives as `session.completed` with `duration_seconds: 0`,
  `transcript: null`, and a terminal `call.status` — see
  [How a failed session surfaces](/webhooks/payloads#how-a-failed-session-surfaces).

  It was previously published here with no producer behind it, so an endpoint
  that allowlisted it silently received nothing for that type. It is now
  rejected with `400 VALIDATION_ERROR`, and existing allowlists were stripped of
  it automatically. An endpoint left with an empty allowlist receives every
  event type.
</Warning>

`webhook.test` is deliberately **not** in this list. It is fired only by the
test endpoint and always reaches the endpoint being tested, regardless of its
allowlist — otherwise "send test" would silently do nothing on exactly the
endpoints most likely to be misconfigured. You still receive it, so handle or
ignore it explicitly.

## How the two controls combine

Both filters must pass for a delivery to happen:

1. **Scope** decides which endpoints are candidates for this event.
2. **Allowlist** decides whether each candidate accepts this event type.

<Warning>
  **Filtering is strict when account fan-out is off.** With
  `include_account_endpoints: false`, an event whose type is not in *any* of the
  agent's endpoint allowlists is **dropped** — it is not forwarded to your
  account-wide endpoint as a fallback.

  This is intentional. Falling back would re-deliver exactly the traffic you
  turned the flag off to stop, and leakage into an integration you were trying
  to quiet is the failure mode the feature exists to prevent. It also keeps
  "where does this event go?" answerable from the agent's flag and its own
  endpoints alone.
</Warning>

This only bites when you use both features at once. An endpoint with an empty
`events` array — the default — receives every type, so the interaction never
arises.

Worked example. Agent `a1b2c3d4…` has `include_account_endpoints: false` and one
endpoint subscribed to `["session.completed"]`:

| Event on that agent | Result                                        |
| ------------------- | --------------------------------------------- |
| `session.completed` | Delivered to the agent's endpoint             |
| `call.transferred`  | **Dropped.** Not sent to the account endpoint |

To receive `call.transferred` too, either add it to the endpoint's `events`, add
a second agent endpoint that subscribes to it, or turn
`include_account_endpoints` back on.

## Webhooks vs lifecycle hooks

These are different mechanisms and people reach for the wrong one.

|               | **Webhooks**                                              | **Lifecycle hooks (PRE\_CALL / POST\_CALL)**                                                     |
| ------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Direction     | Vocobase → you, one-way notification                      | Vocobase → your API, and the response is used                                                    |
| Shape         | Fixed, versioned event payload                            | A request body **you** template                                                                  |
| Failure       | Retried with backoff for hours                            | Can block or annotate the call; `required` hooks fail the step                                   |
| Configured on | Account or agent, in Webhooks                             | The agent, in Actions                                                                            |
| Use for       | Recording outcomes, syncing your CRM, dashboards, billing | Fetching a customer record before the call, pushing structured results into your system after it |

If you want to *know* something happened, use a webhook. If you want to *fetch
or push data as part of the call*, use a lifecycle hook.

## Next steps

<CardGroup cols={2}>
  <Card title="Webhook Setup" icon="gear" href="/webhooks/setup">
    Create endpoints, verify signatures, and send test events.
  </Card>

  <Card title="Webhook Payloads" icon="file-code" href="/webhooks/payloads">
    Full payload structure for each event type.
  </Card>
</CardGroup>
