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

# Webhook management

> Create, list, test, and rotate webhook subscriptions programmatically.

Webhook subscriptions can be managed via the dashboard UI or
programmatically via these endpoints. Both use Supabase JWT
authentication (not API keys).

<Note>
  These endpoints use **dashboard (JWT) authentication**, not the
  `/api/v1/*` API key surface. Your Supabase session must have the
  `webhooks:manage` permission.
</Note>

## Create a subscription

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.yotel.in/api/webhooks \
    -H "Authorization: Bearer SUPABASE_JWT" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://hooks.example.com/yotel",
      "event_types": ["call.ended", "call.recording_ready", "campaign.completed"]
    }'
  ```

  ```python Python theme={null}
  # Dashboard / server-side use — requires Supabase JWT session
  import httpx

  resp = httpx.post(
      "https://api.yotel.in/api/webhooks",
      headers={"Authorization": f"Bearer {jwt}"},
      json={
          "url": "https://hooks.example.com/yotel",
          "event_types": ["call.ended", "call.recording_ready"],
      },
  )
  data = resp.json()
  signing_secret = data["signing_secret"]  # Shown ONCE — save it now
  ```
</CodeGroup>

**Auth:** Supabase JWT + `webhooks:manage`  |  **Status:** `201 Created`

### Request body

| Field         | Type   | Required | Description                                      |
| ------------- | ------ | -------- | ------------------------------------------------ |
| `url`         | string | Yes      | HTTPS endpoint URL (HTTP is rejected)            |
| `test_url`    | string | No       | Separate URL for test-environment webhook events |
| `event_types` | array  | Yes      | One or more event types to subscribe to          |

### Available event types

<AccordionGroup>
  <Accordion title="All 17 event types">
    | Event type                      | Category   |
    | ------------------------------- | ---------- |
    | `call.started`                  | Call       |
    | `call.answered`                 | Call       |
    | `call.ended`                    | Call       |
    | `call.recording_ready`          | Call       |
    | `call.transcript_ready`         | Call       |
    | `ai_session.started`            | AI Session |
    | `ai_session.ended`              | AI Session |
    | `ai_session.transferred`        | AI Session |
    | `ai_session.escalated`          | AI Session |
    | `ai_session.conference_changed` | AI Session |
    | `lead.completed`                | Lead       |
    | `campaign.started`              | Campaign   |
    | `campaign.paused`               | Campaign   |
    | `campaign.resumed`              | Campaign   |
    | `campaign.completed`            | Campaign   |
    | `agent.logged_in`               | Agent      |
    | `agent.logged_out`              | Agent      |
  </Accordion>
</AccordionGroup>

### Response

The response includes a `signing_secret` field that is **shown only
once**. Store it securely — you'll need it to
[verify webhook signatures](/webhooks/signature-verification).

```json theme={null}
{
  "id": "wh_sub_abc123",
  "tenant_id": "tenant_xyz",
  "url": "https://hooks.example.com/yotel",
  "test_url": null,
  "event_types": ["call.ended", "call.recording_ready"],
  "is_active": true,
  "signing_secret": "whsec_abc123...",
  "created_at": "2026-05-18T10:00:00Z",
  "consecutive_failures": 0
}
```

<Warning>
  **Test tenants** can only point webhooks at `localhost`, `127.0.0.1`,
  `*.ngrok.io`, or `*.test.yotel.in`. Production URLs are rejected for
  test tenants.
</Warning>

***

## List subscriptions

<CodeGroup>
  ```bash curl theme={null}
  curl https://api.yotel.in/api/webhooks \
    -H "Authorization: Bearer SUPABASE_JWT"
  ```
</CodeGroup>

**Auth:** Supabase JWT + `webhooks:manage`  |  **Status:** `200 OK`

Query parameter: `include_disabled` (boolean, default `false`) — set to
`true` to include auto-disabled subscriptions.

The `signing_secret` field is **not** returned on list/get — only on
create and rotate.

***

## Delete a subscription

<CodeGroup>
  ```bash curl theme={null}
  curl -X DELETE https://api.yotel.in/api/webhooks/{subscription_id} \
    -H "Authorization: Bearer SUPABASE_JWT"
  ```
</CodeGroup>

**Auth:** Supabase JWT + `webhooks:manage`  |  **Status:** `204 No Content`

***

## Rotate signing secret

Issues a new signing secret for an existing subscription. A **24-hour
grace window** accepts signatures from both the old and new secret,
giving you time to deploy the new secret without downtime.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.yotel.in/api/webhooks/{subscription_id}/rotate \
    -H "Authorization: Bearer SUPABASE_JWT"
  ```
</CodeGroup>

**Auth:** Supabase JWT + `webhooks:manage`  |  **Status:** `200 OK`

The response includes the new `signing_secret` (shown once). During the
grace window, the `X-Yotel-Signature` header contains both signatures:
`sha256=OLD,sha256=NEW` — your verification code should accept either.

***

## Test a subscription

Sends a synchronous test event to your endpoint and reports the result
immediately. This is a dry-run — it bypasses the delivery log.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.yotel.in/api/webhooks/{subscription_id}/test \
    -H "Authorization: Bearer SUPABASE_JWT" \
    -H "Content-Type: application/json" \
    -d '{"event_type": "call.ended"}'
  ```
</CodeGroup>

**Auth:** Supabase JWT + `webhooks:manage`  |  **Status:** `200 OK`

### Request body

| Field            | Type   | Default        | Description                                       |
| ---------------- | ------ | -------------- | ------------------------------------------------- |
| `event_type`     | string | `"call.ended"` | Event type to simulate                            |
| `sample_payload` | object | null           | Custom payload (uses a default sample if omitted) |

### Response

```json theme={null}
{
  "delivered": true,
  "http_status": 200,
  "response_snippet": "{\"ok\":true}",
  "duration_ms": 142,
  "error": null
}
```

***

## View delivery log

Inspect recent deliveries for a subscription — status, response codes,
timing, and retry schedule.

<CodeGroup>
  ```bash curl theme={null}
  curl "https://api.yotel.in/api/webhooks/{subscription_id}/deliveries?limit=50" \
    -H "Authorization: Bearer SUPABASE_JWT"
  ```
</CodeGroup>

**Auth:** Supabase JWT + `webhooks:manage`  |  **Status:** `200 OK`

### Query parameters

| Parameter | Type    | Default | Description                 |
| --------- | ------- | ------- | --------------------------- |
| `limit`   | integer | `100`   | Max rows to return (1–1000) |

### Response

```json theme={null}
[
  {
    "id": "del_abc",
    "subscription_id": "wh_sub_abc123",
    "event_type": "call.ended",
    "event_id": "evt_xyz",
    "payload": { "...": "..." },
    "attempt": 1,
    "status": "delivered",
    "http_status": 200,
    "response_snippet": "{\"ok\":true}",
    "created_at": "2026-05-18T10:04:48Z",
    "delivered_at": "2026-05-18T10:04:48Z",
    "next_retry_at": null,
    "duration_ms": 87
  }
]
```

| Field           | Type           | Description                                                  |
| --------------- | -------------- | ------------------------------------------------------------ |
| `status`        | string         | `"pending"`, `"delivered"`, `"failed"`, or `"dead"`          |
| `attempt`       | integer        | Delivery attempt number (1 = first try)                      |
| `next_retry_at` | string \| null | When the next retry is scheduled (null if delivered or dead) |

***

## Subscription response fields

| Field                  | Type           | Description                                                     |
| ---------------------- | -------------- | --------------------------------------------------------------- |
| `id`                   | string         | Subscription UUID                                               |
| `tenant_id`            | string         | Owning tenant                                                   |
| `url`                  | string         | Delivery endpoint                                               |
| `test_url`             | string \| null | Test environment endpoint                                       |
| `event_types`          | array          | Subscribed event types                                          |
| `is_active`            | boolean        | Whether the subscription is active                              |
| `created_at`           | string         | ISO 8601                                                        |
| `last_success_at`      | string \| null | Last successful delivery                                        |
| `last_failure_at`      | string \| null | Last failed delivery                                            |
| `consecutive_failures` | integer        | Current failure streak                                          |
| `disabled_at`          | string \| null | When auto-disabled (null if active)                             |
| `disabled_reason`      | string \| null | Why it was disabled (e.g., `"auto_disabled_failure_threshold"`) |
