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.
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. |
headers | object | no | Extra headers. string → string map. |
is_active | boolean | no | Default true. |
Response 201
{
"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"
}
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.
List subscriptions
GET /v1/webhooks — returns every subscription on your company.
curl https://api.keebai.com/v1/webhooks \
-H "Authorization: Bearer kbai_pk_xxx"
Response 200
{
"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
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.
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
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.
Dispatch a synthetic event (test)
POST /v1/webhooks/:id/test — sends a synthetic envelope with data._test: true to your URL.
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
{
"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>
curl 'https://api.keebai.com/v1/webhooks/wh_01HXYZ/deliveries?limit=50' \
-H "Authorization: Bearer kbai_pk_xxx"
Response 200
{
"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
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.