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

# Webhooks and events

> Receive signed events from all your tenants at one address.

A partner webhook endpoint is one address of your product. It hears the events of every tenant of your partner account.
Each event names the tenant, so one receiver can serve all of them. Reading needs the scope `webhooks:read`. Creating and
changing needs `webhooks:manage`.

## Register an endpoint

```bash theme={null}
curl -X POST https://api.impellabs.tech/api/v1/partner/webhooks/endpoints/ \
  -H "Authorization: Bearer implpk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.yourcompany.test/impellabs",
    "description": "CRM",
    "enabled_events": ["message.*", "conversation.*", "channel.*"]
  }'
```

| Field | Rule |
| - | - |
| `url` | Required. A public `https` address. Other schemes and private addresses are refused. 500 characters at most. |
| `enabled_events` | Optional. A list of event names, `*`, or a group such as `message.*`. Default `*`. An unknown name answers 400. |
| `description` | Optional. 200 characters at most. |
| `api_version` | Optional. `2026-06-01` (default) or `2026-01-01`. It sets the shape of the body. |

The answer is 201 and holds the signing `secret` once. Store it now.

```json theme={null}
{
  "id": "6e4a2b8c-0000-4000-8000-000000000000",
  "url": "https://hooks.yourcompany.test/impellabs",
  "description": "CRM",
  "status": "active",
  "api_version": "2026-06-01",
  "enabled_events": ["message.*", "conversation.*", "channel.*"],
  "granted_scopes": ["messages:read", "channels:read"],
  "secret_hint": "k3Yz",
  "created_at": "2026-01-15T09:30:00+00:00",
  "secret": "whsec_..."
}
```

`granted_scopes` come from the key that made the endpoint. An endpoint hears an event only when its scopes allow the
event. A console session grants all scopes.

| Event group | Scope needed |
| - | - |
| `message.*`, `conversation.*`, `handoff.requested` | `messages:read` |
| `channel.*`, `template.*` | `channels:read` |
| `lead.*` | `leads:read` |
| `knowledge.*` | `knowledge:read` |
| Other events | None |

## Manage endpoints

| Call | Use |
| - | - |
| `GET /api/v1/partner/webhooks/endpoints/` | List your endpoints. |
| `PATCH /api/v1/partner/webhooks/endpoints/{endpoint_id}/` | Change `enabled_events`, `granted_scopes`, `description` or `status`. `status` `active` switches it on and clears its failure count. Any other value switches it off. |
| `DELETE /api/v1/partner/webhooks/endpoints/{endpoint_id}/` | Remove the endpoint. The answer is 204. |
| `POST /api/v1/partner/webhooks/endpoints/{endpoint_id}/rotate-secret/` | Make a new secret. It is shown once. The old secret signs beside the new one for 24 hours. |
| `GET /api/v1/partner/webhooks/deliveries/` | Show the last 200 deliveries: `event_type`, `tenant_ref`, `status`, `attempts` and `failure_code`. |

## Event names

An endpoint can hear the events that ImpelLabs sends for a workspace, with two names changed for you:
`conversation.created` (a conversation starts) and `conversation.completed` (a conversation closes). Use these names in
`enabled_events`.

The events with an object in the shape of the Partner API are:

| Event | `data.object` |
| - | - |
| `message.received`, `message.sent`, `message.delivered`, `message.read`, `message.failed` | The message object. See [Conversations](https://docs.impellabs.tech/partners/conversations.md). |
| `conversation.created`, `conversation.completed`, `handoff.requested` | The conversation object. The last two add `reason`. |
| `channel.status_changed` | The number or Page object, with `connection` and `removed`. |
| `channel.history_synced` | `phone_number_id`, `phase`, `progress`. |
| `channel.connected` | The number that a hosted sign-up connected. |
| `template.status_changed` | `name`, `language`, `status`, `category`, `rejected_reason`, `waba_id`. |

Other events of the workspace carry their plain data in `data.object`. The workspace [event types](https://docs.impellabs.tech/api/events.md) page
describes them. See [WhatsApp events](https://docs.impellabs.tech/partners/channels/whatsapp/events.md) for examples.

## The request

ImpelLabs sends a `POST` with a JSON body.

```json theme={null}
{
  "id": "evt_...",
  "type": "message.received",
  "api_version": "2026-06-01",
  "created_at": "2026-01-15T10:04:01Z",
  "tenant": "2f1c0b9e-0000-4000-8000-000000000000",
  "tenant_ref": "acme-001",
  "partner": "your-partner-slug",
  "data": { "object": { "object": "message" } },
  "correlation": { "request_id": "", "trace_id": "", "job_id": "" }
}
```

`tenant` is the workspace id and `tenant_ref` is your `external_ref`. Version `2026-01-01` has no `tenant_ref` and no
`partner`. The `id` is the same for the same event, so use it to drop a repeat.

Headers:

| Header | Value |
| - | - |
| `webhook-id` | The event id. |
| `webhook-timestamp` | Unix time in seconds. |
| `webhook-signature` | `v1,` and a base64 signature. During a rotation it holds two signatures with a space between. |
| `X-Impel-Signature` | `t=<time>,v1=<hex>`. Same secret, hex form. |
| `X-Impel-Event`, `X-Impel-Event-Id`, `X-Impel-Delivery-Id`, `X-Impel-Attempt`, `X-Impel-Api-Version` | The event name, the event id, the delivery id, the attempt number and the version. |

## Check the signature

Sign the text `{webhook-id}.{webhook-timestamp}.{body}` with HMAC-SHA256. The key is the base64 decoding of the secret
without the `whsec_` prefix. Compare the base64 result with the `v1,` value of `webhook-signature`.

```python theme={null}
import base64, hashlib, hmac

def valid(secret, msg_id, stamp, body, header):
    key = base64.b64decode(secret.removeprefix("whsec_"))
    mac = hmac.new(key, f"{msg_id}.{stamp}.".encode() + body, hashlib.sha256).digest()
    expected = "v1," + base64.b64encode(mac).decode()
    return any(hmac.compare_digest(expected, s) for s in header.split(" "))
```

Use the raw body, byte for byte. Reject a timestamp that is far from your clock.

## Answers and retries

Answer with a status from 200 to 299 within 10 seconds. Any other answer fails the delivery. ImpelLabs repeats a delivery
when your address answers 5xx, 408 or 429, or does not answer. The waits are 1 minute, 5 minutes, 30 minutes and 2
hours. A 4xx answer, other than 408 and 429, is not repeated. After 15 failed deliveries in a row, the endpoint switches
itself off with `status` `disabled`. Change it back with `PATCH` and `status` `active` after you fix the address.


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