Webhooks
poolse POSTs JSON to a URL on your backend when something happens in
your tenant. Every endpoint here takes an API key with the
webhooks:admin scope. Without it you get 403 forbidden. You can also
manage webhooks on the dashboard; see Webhooks.
The webhook object
{
"id": "7d3b9f2e-0a4c-4e6b-8d1f-3c5a7e9b1d24",
"tenant_id": "65787825-3152-4c1e-9b8a-0d6f4e2a7c11",
"name": "main",
"url": "https://example.com/webhooks/poolse",
"events": ["message.created", "call.finished"],
"is_active": true,
"last_delivery_at": "2026-06-02T08:13:56.123456Z",
"inserted_at": "2026-06-01T10:00:00.000000Z",
"updated_at": "2026-06-01T10:00:00.000000Z"
}
| Field | Type | Description |
|---|---|---|
name | string | 1–255 characters. |
url | string | Where deliveries go. Must be an http or https URL with a host. |
events | string[] | The events to send. An empty list sends every event. |
is_active | boolean | false pauses the webhook: no new deliveries are created. |
last_delivery_at | string | null | Time of the last successful (2xx) delivery. |
The signing secret is never part of this object.
Create a webhook
/v1/webhooks| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | |
url | string | yes | |
events | string[] | yes | Event names from Events. Send [] for every event. |
is_active | boolean | no | Default true. |
Response
201:
{
"webhook": {
"id": "7d3b9f2e-0a4c-4e6b-8d1f-3c5a7e9b1d24",
"tenant_id": "65787825-3152-4c1e-9b8a-0d6f4e2a7c11",
"name": "main",
"url": "https://example.com/webhooks/poolse",
"events": [],
"is_active": true,
"last_delivery_at": null,
"inserted_at": "2026-06-01T10:00:00.000000Z",
"updated_at": "2026-06-01T10:00:00.000000Z"
},
"signing_secret": "q3Jx0b8m1F6Z…="
}
curl -X POST https://api.poolse.dev/v1/webhooks \
-H "Authorization: Bearer $POOLSE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"main","url":"https://example.com/webhooks/poolse","events":["message.created"]}'
Errors
| Code | Status | When |
|---|---|---|
validation_failed | 422 | A field is missing or invalid, or an event name is unknown. details names the field. |
webhooks_not_in_plan | 402 | Your plan does not include webhooks. |
webhook_count_cap_exceeded | 402 | You have as many webhooks as your plan allows. details: { count, cap }. |
subscription_inactive | 402 | Your subscription is past due or cancelled. |
create_failed | 422 | Any other failure. |
List webhooks
/v1/webhooks200: { "data": [ Webhook, … ] }, newest first. Not paginated.
Update a webhook
/v1/webhooks/:idSend any of name, url, events, is_active. The signing secret
cannot be changed.
curl -X PATCH https://api.poolse.dev/v1/webhooks/$WEBHOOK_ID \
-H "Authorization: Bearer $POOLSE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"is_active":false}'
200: the updated webhook.
| Code | Status | When |
|---|---|---|
not_found | 404 | No such webhook in the tenant. |
validation_failed | 422 | A field is invalid. |
Delete a webhook
/v1/webhooks/:idDeletes the webhook and its delivery history. Deliveries not yet sent
are dropped. 204 on success. 404 not_found if there is no such
webhook.
Deliveries
Each event sent to a webhook is one delivery, retried until it succeeds or runs out of attempts.
The delivery object
{
"id": "b81e4c0a-9d2f-4a6b-8c3e-7f1d5a9b2e60",
"webhook_id": "7d3b9f2e-0a4c-4e6b-8d1f-3c5a7e9b1d24",
"event": "message.created",
"payload": { "type": "message.created", "tenant_id": "65787825-…", "data": { } },
"attempt": 1,
"response_status": 200,
"response_body": "ok",
"last_error": null,
"next_attempt_at": null,
"delivered_at": "2026-06-02T08:13:56.123456Z",
"failed_at": null,
"inserted_at": "2026-06-02T08:13:55.123456Z",
"updated_at": "2026-06-02T08:13:56.123456Z"
}
| Field | Type | Description |
|---|---|---|
event | string | The event name. |
payload | object | The stored body without id and created_at, which are added at send time. |
attempt | integer | Attempts made so far. |
response_status, response_body | integer, string | null | From the latest attempt. |
last_error | string | null | Why the latest attempt failed, for example HTTP 500. |
next_attempt_at | string | null | When the next retry is due. |
delivered_at | string | null | Set when a 2xx came back. |
failed_at | string | null | Set when every attempt failed. No more retries happen. |
List deliveries
/v1/webhooks/:id/deliveries200: { "data": [ Delivery, … ] }, the 50 most recent, newest first.
There is no pagination and no way to read older deliveries. Deliveries
are kept for 30 days. 404 not_found if there is no such webhook.
Replay a delivery
/v1/webhooks/:id/deliveries/:delivery_id/replaySends the same event again as a new delivery, with a new id. The
original delivery is not changed. A replay is sent even if the webhook
is paused (is_active: false) and even when your daily delivery quota is
used up. It does count toward the quota.
201: the new delivery, not yet attempted. Watch the deliveries list for
the result.
| Code | Status | When |
|---|---|---|
not_found | 404 | No such webhook, or no such delivery on it. |
replay_failed | 422 | The new delivery could not be created. |
What poolse sends
An HTTP POST with a JSON body:
{
"id": "b81e4c0a-9d2f-4a6b-8c3e-7f1d5a9b2e60",
"type": "message.created",
"tenant_id": "65787825-3152-4c1e-9b8a-0d6f4e2a7c11",
"created_at": "2026-06-02T08:13:55.123456Z",
"data": { }
}
id is the delivery id and stays the same across retries: use it to
skip duplicates. A replay has a new id. data depends on the event:
see Webhook events.
Headers:
| Header | Value |
|---|---|
content-type | application/json |
user-agent | poolse-webhook/1 |
x-poolse-signature | t=<unix seconds>,v1=<hex HMAC> |
x-poolse-event-id | The delivery id. |
x-poolse-event-type | The event name. |
x-poolse-delivery-attempt | 1 for the first attempt, 2 for the first retry, and so on. |
Verify the signature. v1 is the lowercase hex HMAC-SHA256 of
<t>.<raw body>, keyed with the signing secret exactly as returned at
creation. Compute it over the raw bytes you received, compare in constant
time, and reject old t values. Code is in
Webhook verification.
Retries
- A 2xx response marks the delivery delivered.
- Any other status, a timeout or a network error is a failure. poolse retries after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours: 6 attempts in all. After the sixth failure the delivery is marked failed and is not retried unless you replay it.
When events are dropped
poolse does not create a delivery, and does not queue it for later, when:
- your plan's daily delivery quota is used up (a rolling 24 hours);
- your tenant produces more than 1,000 webhook events in a minute (an abuse limit, the same on every plan);
- the webhook is paused.
Nothing tells your endpoint about a dropped event. See Limits & quotas for plan quotas.
Events
| Event | Sent when |
|---|---|
message.created | A message is sent. |
message.updated | A message is edited. |
message.deleted | A message is deleted. |
mention.created | A message mentions a user. One event per mentioned user. |
conversation.created | A conversation is created. |
conversation.updated | A conversation is updated with PATCH /v1/conversations/:id. |
member.added | A user is added to a conversation. |
member.removed | A user leaves or is removed. |
call.started | A call is placed or a room opened. |
call.answered | The first person rung picks up. Calls only. |
call.finished | A call or room ends. |
The data of each event is documented in
Webhook events.