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

# Manage via API

> CRUD for webhook subscriptions directly over /v1/webhooks. Same operations as the CLI, nothing to install.

All endpoints accept and return JSON, require a PAT in `Authorization: Bearer <token>`, and accept `Content-Type: application/json`. Base URL: `https://api.keebai.com/v1`.

| Method   | Path                             | Scope             |
| -------- | -------------------------------- | ----------------- |
| `POST`   | `/v1/webhooks`                   | `webhooks:manage` |
| `GET`    | `/v1/webhooks`                   | `webhooks:read`   |
| `GET`    | `/v1/webhooks/:id`               | `webhooks:read`   |
| `PATCH`  | `/v1/webhooks/:id`               | `webhooks:manage` |
| `POST`   | `/v1/webhooks/:id/rotate-secret` | `webhooks:manage` |
| `DELETE` | `/v1/webhooks/:id`               | `webhooks:manage` |
| `POST`   | `/v1/webhooks/:id/test`          | `webhooks:manage` |
| `GET`    | `/v1/webhooks/:id/deliveries`    | `webhooks:read`   |

## Create a subscription

`POST /v1/webhooks` — returns the raw secret **only once**.

```bash theme={null}
curl -X POST https://api.keebai.com/v1/webhooks \
  -H "Authorization: Bearer kbai_pk_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production CRM",
    "url": "https://hooks.example.com/keebai",
    "events": ["whatsapp.message.received", "whatsapp.channel.connected"],
    "headers": { "X-Tenant-Origin": "crm" },
    "is_active": true
  }'
```

**Body**

| Field       | Type      | Required | Validation                                                            |
| ----------- | --------- | -------- | --------------------------------------------------------------------- |
| `name`      | string    | yes      | 3–120 chars.                                                          |
| `url`       | string    | yes      | Valid HTTPS URL, ≤2048 chars. `http://` is rejected.                  |
| `events`    | string\[] | yes      | ≥1 event, unique, must belong to the [catalog](/dev/webhooks/events). |
| `headers`   | object    | no       | Extra headers. `string → string` map.                                 |
| `is_active` | boolean   | no       | Default `true`.                                                       |

**Response 201**

```json theme={null}
{
  "id": "wh_01HXYZ...",
  "name": "Production CRM",
  "url": "https://hooks.example.com/keebai",
  "events": ["whatsapp.message.received", "whatsapp.channel.connected"],
  "headers": { "X-Tenant-Origin": "crm" },
  "is_active": true,
  "secret_prefix": "whsec_8f2c5b3a",
  "secret": "whsec_8f2c5b3a9d4e7c1f...",
  "created_at": "2026-04-27T12:34:56.789Z",
  "updated_at": "2026-04-27T12:34:56.789Z"
}
```

<Warning>
  `secret` is returned **only in this response**. You will never see it again. Store it in your secret manager. If you lose it, you have to rotate (`POST /v1/webhooks/:id/rotate-secret`), which invalidates the old one.
</Warning>

## List subscriptions

`GET /v1/webhooks` — returns every subscription on your company.

```bash theme={null}
curl https://api.keebai.com/v1/webhooks \
  -H "Authorization: Bearer kbai_pk_xxx"
```

**Response 200**

```json theme={null}
{
  "data": [
    {
      "id": "wh_01HXYZ...",
      "name": "Production CRM",
      "url": "https://hooks.example.com/keebai",
      "events": ["whatsapp.message.received"],
      "is_active": true,
      "secret_prefix": "whsec_8f2c5b3a",
      "failure_count": 0,
      "disabled_reason": null,
      "last_delivery_at": "2026-04-27T13:00:00Z",
      "created_at": "2026-04-27T12:34:56Z",
      "updated_at": "2026-04-27T12:34:56Z"
    }
  ]
}
```

`secret` is **not** included in list or get-by-id. Only `secret_prefix` (the first 12 chars), which is enough to visually identify which key you're using.

## Get a subscription

`GET /v1/webhooks/:id`

```bash theme={null}
curl https://api.keebai.com/v1/webhooks/wh_01HXYZ \
  -H "Authorization: Bearer kbai_pk_xxx"
```

Same shape as a list item.

## Update a subscription

`PATCH /v1/webhooks/:id` — every field is optional.

```bash theme={null}
curl -X PATCH https://api.keebai.com/v1/webhooks/wh_01HXYZ \
  -H "Authorization: Bearer kbai_pk_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "events": ["whatsapp.message.received", "whatsapp.message.failed"],
    "is_active": true
  }'
```

Useful for:

* Reactivating a subscription that was auto-disabled by excessive failures (`is_active: true`).
* Adding or removing events without recreating it (preserves the secret).
* Changing the URL (preserves the secret).

## Rotate secret

`POST /v1/webhooks/:id/rotate-secret`

```bash theme={null}
curl -X POST https://api.keebai.com/v1/webhooks/wh_01HXYZ/rotate-secret \
  -H "Authorization: Bearer kbai_pk_xxx"
```

**Response 200** — same shape as `create`, with a new `secret`. **The old one is invalidated immediately.** For zero-downtime rotation, see [security](/dev/webhooks/security#rotación-de-secret).

## Dispatch a synthetic event (test)

`POST /v1/webhooks/:id/test` — sends a synthetic envelope with `data._test: true` to your URL.

```bash theme={null}
curl -X POST https://api.keebai.com/v1/webhooks/wh_01HXYZ/test \
  -H "Authorization: Bearer kbai_pk_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "event_type": "whatsapp.message.received" }'
```

**Response 202**

```json theme={null}
{
  "accepted": true,
  "event_id": "evt_01HXYZ_TEST..."
}
```

Useful in CI: chain a `webhooks test` after deploying your endpoint to verify that HMAC verification still works.

## List deliveries

`GET /v1/webhooks/:id/deliveries?limit=50&cursor=<delivery_id>`

```bash theme={null}
curl 'https://api.keebai.com/v1/webhooks/wh_01HXYZ/deliveries?limit=50' \
  -H "Authorization: Bearer kbai_pk_xxx"
```

**Response 200**

```json theme={null}
{
  "data": [
    {
      "id": "dlv_01HXYZ...",
      "event_id": "evt_01HXYZ...",
      "event_type": "whatsapp.message.received",
      "status": "success",
      "status_code": 200,
      "attempts": 1,
      "duration_ms": 142,
      "last_error": null,
      "delivered_at": "2026-04-27T13:00:00.142Z",
      "created_at": "2026-04-27T13:00:00.000Z"
    }
  ],
  "next_cursor": "dlv_..."
}
```

`status`:

* `pending` — queued, not yet attempted.
* `success` — 2xx response.
* `failed` — non-retryable failure (4xx other than 408/429), or retries are exhausted but still in the retry window.
* `dead` — exceeded the 5 attempts, no further retries.

## Delete a subscription

`DELETE /v1/webhooks/:id`

```bash theme={null}
curl -X DELETE https://api.keebai.com/v1/webhooks/wh_01HXYZ \
  -H "Authorization: Bearer kbai_pk_xxx"
```

**Response 204** — no body. Future events are not sent to this URL.

## Common errors

| Status | Code                  | Cause                                                                                                                          |
| ------ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `401`  | `UNAUTHENTICATED`     | PAT is invalid, expired, or revoked.                                                                                           |
| `403`  | `INSUFFICIENT_SCOPE`  | The PAT is missing `webhooks:read` or `webhooks:manage` for the operation.                                                     |
| `400`  | `VALIDATION_ERROR`    | Invalid body (non-HTTPS URL, event\_type outside the catalog, name too short, etc.). The `details` field lists the violations. |
| `404`  | `WEBHOOK_NOT_FOUND`   | Id doesn't exist or belongs to another company.                                                                                |
| `429`  | `RATE_LIMIT_EXCEEDED` | Public API throttling. Retry after `Retry-After`.                                                                              |

## Auto-disable

If a subscription accumulates **50 consecutive `dead` deliveries**, we mark it `is_active: false` with `disabled_reason: 'excessive_failures'` and email the owner. Reactivate via `PATCH /v1/webhooks/:id { "is_active": true }` once you've fixed the endpoint.

This stops an endpoint under maintenance from collecting noise for hours. The counter resets on the next successful delivery.
