> ## 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.

# Outbound webhooks

> Receive signed HTTP notifications when things happen in Keebai — messages, conversations, CRM records, and appointments. 32 events across five domains.

**Outbound webhooks** are HTTP calls Keebai makes to your server when something happens in your account — an inbound message, a delivery receipt, a CRM ticket moving through the pipeline, an appointment being cancelled. They're the flip side of the API: instead of you polling, Keebai pushes the event to you.

There are **32 events** across five domains — WhatsApp, Instagram, conversations, CRM, and scheduling. The full list with payloads is in the [event catalog](/dev/webhooks/events).

<Note>
  Not to be confused with the **inbound** webhooks Meta sends to Keebai. Those are configured by Keebai in the Meta dashboard when you connect a channel via Embedded Signup; you don't touch them.
</Note>

## Use cases

<CardGroup cols={2}>
  <Card title="Sync messages to your CRM" icon="rotate">
    Receive every `whatsapp.message.received` and store it in your system along with the conversation, contact, and content.
  </Card>

  <Card title="Track delivery / read" icon="eye">
    Subscribe to `whatsapp.message.delivered` / `.read` / `.failed` to report delivery rates in your dashboard.
  </Card>

  <Card title="React to templates" icon="bell">
    Receive `whatsapp.template.status_updated` when Meta approves or rejects a template, and notify your marketing team.
  </Card>

  <Card title="Automated provisioning" icon="plug">
    Listen for `whatsapp.channel.connected` to trigger your onboarding flow the moment a channel is ready.
  </Card>

  <Card title="Mirror the CRM pipeline" icon="address-book">
    Subscribe to `crm.ticket.stage_changed` and `crm.ticket.closed` to keep an external CRM, a BI warehouse, or a Slack channel in step with the funnel.
  </Card>

  <Card title="Sync the calendar" icon="calendar-check">
    `scheduling.appointment.created` / `.rescheduled` / `.cancelled` carry the branch, service, professional, and customer inline — enough to update an external calendar without a follow-up call.
  </Card>

  <Card title="Escalation alerts" icon="user-check">
    `conversation.escalated_to_human` fires on the transition, once, when a ticket lands in a human-attended stage. Page whoever needs to pick it up.
  </Card>
</CardGroup>

## Scopes

| Scope             | Grants                                                                                 |
| ----------------- | -------------------------------------------------------------------------------------- |
| `webhooks:read`   | [List subscriptions](/dev/webhooks/manage) and read delivery history.                  |
| `webhooks:manage` | Create, update, rotate the signing secret, delete subscriptions, and fire test events. |

Rotating a secret invalidates the old one immediately, so `webhooks:manage` is the scope that can break your own receiver. A dashboard that only inspects delivery history needs `webhooks:read` alone.

## How it works

<Steps>
  <Step title="Register an HTTPS URL">
    Via CLI (`keebai webhooks new`) or via API (`POST /v1/webhooks`). Specify the events you care about and optionally extra headers.
  </Step>

  <Step title="Keebai returns a secret">
    Once. You use it to verify that each delivery actually came from us (HMAC-SHA256 signature). **It cannot be retrieved later** — store it in your secret manager.
  </Step>

  <Step title="When an event happens, we deliver it">
    `POST` call to your URL with the standard JSON envelope and signature headers. If your endpoint responds 2xx, we mark the delivery as successful. If it responds 5xx / timeout / 408 / 429, we retry with exponential backoff (5 attempts, up to \~3h).
  </Step>

  <Step title="After 50 consecutive failures, we auto-disable">
    To avoid hammering an endpoint that's been down for a long time. You receive an email and the subscription is left with `is_active: false` and `disabled_reason: 'excessive_failures'`. Reactivate via `PATCH`.
  </Step>
</Steps>

## Envelope

Every event shares the same outer shape. The difference is the `data` field, discriminated by `type`.

```json theme={null}
{
  "id": "evt_01HXYZABC...",
  "type": "whatsapp.message.received",
  "occurred_at": "2026-04-27T12:34:56.789Z",
  "company_id": "comp_xxx",
  "project_id": "65b1c2d3e4f5a6b7c8d9e0f1",
  "channel_id": "ch_xxx",
  "data": {
    "phone_number_id": "1234567890",
    "from": "+56912345678",
    "to": "+56987654321",
    "message_id": "wamid.HBgL...",
    "type": "text",
    "text": { "body": "Hola!" },
    "timestamp": "1714214096"
  }
}
```

| Field                    | Type                     | Description                                                                                    |
| ------------------------ | ------------------------ | ---------------------------------------------------------------------------------------------- |
| `id`                     | string                   | Unique event ID. Stable: if we retry the delivery, the same ID arrives. Use it to deduplicate. |
| `type`                   | enum                     | Discriminator from the catalog. See [events](/dev/webhooks/events).                            |
| `occurred_at`            | ISO-8601                 | Event timestamp (UTC).                                                                         |
| `company_id`             | string                   | Your Keebai company ID. Route on it when one endpoint serves several tenants.                  |
| `project_id`             | string \| absent         | Project the event belongs to. Only on the message and conversation events — see below.         |
| `channel_id`             | string \| null \| absent | Channel associated with the event. Only on the message and conversation events.                |
| `dev_tenant_id`          | string \| absent         | Reseller flow only. Internal identifier — see the warning below.                               |
| `dev_tenant_external_id` | string \| null \| absent | Reseller flow only. The external id you supplied when creating the tenant.                     |
| `data`                   | object                   | Event-specific payload. Shape varies by `type`.                                                |

<Warning>
  `dev_tenant_id` is an **internal identifier** and ships only on the inbound-message and
  conversation events for channels onboarded through the reseller flow. It is documented here because it is on the
  wire today, not because it is part of the contract: do not persist it, do not key on it, and
  expect it to disappear. If you need a stable handle for a tenant, use
  `dev_tenant_external_id` — the value you chose yourself.
</Warning>

## Delivery headers

Every `POST` to your endpoint includes:

| Header                 | Example                     | Usage                                                                                      |
| ---------------------- | --------------------------- | ------------------------------------------------------------------------------------------ |
| `X-Keebai-Signature`   | `t=1714214100,v1=8f2c...`   | HMAC-SHA256 signature over `${t}.${rawBody}`. See [security](/dev/webhooks/security).      |
| `X-Keebai-Event-Id`    | `evt_01HXYZ...`             | Same as `body.id`. Handy for dedup without parsing the body.                               |
| `X-Keebai-Event-Type`  | `whatsapp.message.received` | Discriminator.                                                                             |
| `X-Keebai-Delivery-Id` | `dlv_...`                   | Delivery attempt ID. Changes between retries of the same event.                            |
| `X-Keebai-Timestamp`   | `1714214100`                | Unix seconds when the signature was generated. Same value as `t=` in `X-Keebai-Signature`. |
| `User-Agent`           | `Keebai-Webhooks/1.0`       |                                                                                            |
| `Content-Type`         | `application/json`          |                                                                                            |
| `+custom headers`      |                             | If you configured `headers` when creating the subscription, they're included too.          |

## Guarantees and limitations

<CardGroup cols={2}>
  <Card title="At-least-once delivery">
    If your endpoint times out or returns 5xx, we retry. Your endpoint **must be idempotent**: dedup by `event_id`.
  </Card>

  <Card title="No ordering guarantee">
    Events from the same channel can arrive out of chronological order (especially with retries). Use `occurred_at` to sort.
  </Card>

  <Card title="10s timeout">
    If your endpoint doesn't respond within 10 seconds, we count a timeout and retry. Process fast and return 2xx; if you need heavy work, push it to your own queue.
  </Card>

  <Card title="HTTPS required">
    We don't accept `http://` URLs. Use a valid certificate (Let's Encrypt works).
  </Card>
</CardGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Event catalog" icon="list" href="/dev/webhooks/events">
    Full list of `type` values and the `data` shape for each one.
  </Card>

  <Card title="Signature verification" icon="shield" href="/dev/webhooks/security">
    How to validate `X-Keebai-Signature` with HMAC-SHA256 in Node, Python, and PHP.
  </Card>

  <Card title="Manage via CLI" icon="terminal" href="/dev/webhooks/cli">
    Quickstart with `keebai webhooks new/list/test/rotate/delete`.
  </Card>

  <Card title="Manage via API" icon="code" href="/dev/webhooks/manage">
    REST endpoints `/v1/webhooks` to create, list, rotate, delete, and test.
  </Card>
</CardGroup>
