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

# Agency Webhooks

> Register a webhook for any client workspace in your Tuco agency — or for every client in one call — without holding each client's API key.

<Note>
  Webhooks stay workspace-scoped: each client gets its own row, its own scope and
  its own secret. What changes is **who can create it** — your agency key can,
  for any workspace you own, so registration happens in the same script that
  creates the client.
</Note>

## Endpoint

* **Method**: `POST` to create, `GET` to read, `DELETE /api/agency/webhooks/{id}` to remove
* **Path**: `/api/agency/webhooks`
* **Auth**: `Authorization: Bearer tucoagency_xxxxxxxxxxxxx` ([agency key](/api-reference/agency-api-keys))

## Body

| Field | Type | Required | Notes |
| - | - | - | - |
| `workspaceId` | string | **yes** | A client `clerkOrgId`, or the literal `"all"` for every client in your agency. No default — a typo must not fan out to everyone |
| `url` | string | yes | Must parse as a URL |
| `events` | string\[] | yes | One or more of the ten events. An unknown event is rejected and named |
| `name` | string | yes | Shown in the dashboard |
| `description` | string | no | |
| `secret` | string | no | Supply your own so **one receiver can verify every client** with a single signing secret. Omit and each client gets its own random one |
| `authHeader` | string | no | Sent verbatim as `Authorization` on each delivery — for endpoints that 401 without one |

## One client

```bash theme={null}
curl https://app.tuco.ai/api/agency/webhooks \
  -H "Authorization: Bearer tucoagency_xxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "workspaceId": "org_3KKJKupHJ8Y9I2NXVAga0c6ZqgT",
    "url": "https://yourapp.com/hooks/tuco",
    "events": ["message.reply", "conversation.handover"],
    "name": "Acme Roofing → our inbox"
  }'
```

```json theme={null}
{
  "agencyId": "user_3H08IRicrVUYLDZRyOh4jKRVwrx",
  "created": 1,
  "webhooks": [
    {
      "id": "6ac52ed996359b3643c71939",
      "workspaceId": "org_3KKJKupHJ8Y9I2NXVAga0c6ZqgT",
      "workspaceName": "Acme Roofing",
      "secret": "k3K…"
    }
  ],
  "warning": "Save these secrets now. They are not shown again."
}
```

## Every client, one call

This is the call that removes "ten clients means ten registrations". Supply your
own `secret` and a single receiver can verify all of them.

```bash theme={null}
curl https://app.tuco.ai/api/agency/webhooks \
  -H "Authorization: Bearer tucoagency_xxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "workspaceId": "all",
    "url": "https://yourapp.com/hooks/tuco",
    "events": ["message.reply"],
    "name": "All clients → our inbox",
    "secret": "one-secret-i-verify-everything-with"
  }'
```

Each client still gets its own webhook row, so every delivery carries exactly one
tenant's data. Read `workspaceId` off the payload to route it.

## Which clients are not wired up yet

```bash theme={null}
curl https://app.tuco.ai/api/agency/webhooks \
  -H "Authorization: Bearer tucoagency_xxxxxxxxxxxxx"
```

```json theme={null}
{
  "agencyId": "user_3H08IRicrVUYLDZRyOh4jKRVwrx",
  "webhooks": [
    {
      "id": "6ac52ed996359b3643c71939",
      "workspaceId": "org_3KKJKupHJ8Y9I2NXVAga0c6ZqgT",
      "workspaceName": "Acme Roofing",
      "url": "https://yourapp.com/hooks/tuco",
      "events": ["message.reply"],
      "name": "Acme Roofing → our inbox",
      "isActive": true,
      "failureCount": 0
    }
  ],
  "workspacesWithoutWebhook": [
    { "clerkOrgId": "org_3KKJL4Dnbq…", "name": "Vital Cryotherapy" }
  ]
}
```

`workspacesWithoutWebhook` is the useful read: the clients that would silently
receive nothing. Secrets are never returned here — they are shown once, at
creation.

Add `?workspaceId=org_…` to scope the read to one client.

## Removing one

```bash theme={null}
curl -X DELETE https://app.tuco.ai/api/agency/webhooks/6ac52ed996359b3643c71939 \
  -H "Authorization: Bearer tucoagency_xxxxxxxxxxxxx"
```

Unlike deleting a line, this **is** available to an API key — a webhook is
re-created with one call, so it is not an irreversible action.

## Line-health events

Ten events are open to anyone. Two more — `line.down` and `line.recovered` —
are not, and they are the ones an agency usually wants most.

A line going quiet is operational detail. Sent to the end client it is noise
they cannot act on, so Tuco never emails or webhooks it to them. An agency is a
different reader: for its clients it **is** the support desk, so these two
events are available to agencies Tuco has switched on.

<Note>
  Switched on automatically when your agency is **invoiced** — Tuco bills you, not
  your clients, so you are already the operator for every line in the group. On
  Stripe billing, ask us and we will enable it by hand without changing how you pay.
</Note>

| Event | Fires when |
| - | - |
| `line.down` | A line stops sending and the health check confirms it |
| `line.recovered` | That same line starts working again |

```bash theme={null}
curl https://app.tuco.ai/api/agency/webhooks \
  -H "Authorization: Bearer tucoagency_xxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "workspaceId": "all",
    "url": "https://yourapp.com/hooks/tuco-line-health",
    "events": ["line.down", "line.recovered"],
    "name": "Line health → our on-call"
  }'
```

### Payload

Same envelope as every other event — the line-specific fields sit under `data`.

```json theme={null}
{
  "event": "line.down",
  "timestamp": "2026-10-07T04:11:52.610Z",
  "workspaceId": "org_3KKJKupHJ8Y9I2NXVAga0c6ZqgT",
  "contactOwnerEmail": null,
  "ghlContactId": null,
  "ghlLocationId": null,
  "hsPortalId": null,
  "hsContactId": null,
  "data": {
    "lineId": "68c1f0a7d2b4e91f3c7a55e2",
    "workspaceId": "org_3KKJKupHJ8Y9I2NXVAga0c6ZqgT",
    "workspaceName": "Acme Roofing",
    "agencyId": "user_3H08IRicrVUYLDZRyOh4jKRVwrx",
    "identifier": "+14155550134",
    "channelType": "phone",
    "lineName": "Acme Sales 1",
    "at": "2026-10-07T04:11:52.610Z"
  }
}
```

The top-level `null`s are the shared CRM fields every event carries; they are
not meaningful here. Read `data`.

| Field | Notes |
| - | - |
| `lineId` | The line that went down. Pass it to [`GET /api/lines`](/api-reference/endpoint/get-lines) for its current state |
| `workspaceId` / `workspaceName` | Which client. Both are present so one receiver can route across the whole group without a second lookup |
| `identifier` | The line's phone or email |
| `channelType` | `phone` or `email` |
| `lineName` | Display name, or `null` if the line has none |
| `at` | When the health check confirmed it (UTC ISO) |

The payload carries what an operator can act on and nothing else — no failure
strings, no hostnames, no ports. Those stay internal.

Signed and retried exactly like every other event — see
[Message Webhooks](/api-reference/message-webhooks) for verifying
`X-Tuco-Signature`.

### Two ways this is gated

Both have to hold:

1. **Agency key only.** A workspace key (`tuco_…`) cannot subscribe to these
   however the body is crafted — the workspace route never offers them.
2. **Your agency has to be enabled.** If it is not, the create returns `400`
   with a reason rather than a flat "invalid event", so you can tell a typo
   from a feature that is simply not on yet:

   ```json theme={null}
   {
     "error": "line.down is only available to agencies Tuco has enabled it for, and only on an agency API key. Ask us to switch it on.",
     "code": "INVALID_WEBHOOK",
     "lineHealthEnabled": false
   }
   ```

Switching an agency off stops delivery immediately, even though its
subscriptions still exist — the check runs at send time, not at subscribe time.

## Relationship to the workspace route

[`POST /api/webhooks`](/api-reference/endpoint/create-webhook) still does the same
job with that one client's workspace key, and both routes share one validator —
a URL or event list rejected by one is rejected by the other, identically. Use the
workspace route when your code already holds that client's key; use this one when
you are operating across the group.

## Error responses

| Status | Code | When |
| - | - | - |
| `400` | `WORKSPACE_REQUIRED` | `workspaceId` missing. There is deliberately no default |
| `400` | `INVALID_WEBHOOK` | Bad URL, empty or unknown `events`, or missing `name`. The body lists `validEvents` |
| `400` | `NO_WORKSPACES` | `"all"` with no client workspaces yet |
| `400` | `INVALID_ID` | Malformed id on DELETE |
| `401` | `UNAUTHORIZED` | Missing, unknown, revoked or expired key |
| `403` | `AGENCY_KEY_REQUIRED` | A workspace key (`tuco_…`) was used |
| `403` | `NOT_YOUR_WORKSPACE` | `workspaceId` is not in your agency |
| `404` | `NOT_FOUND` | The webhook is not one of your agency's |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.