Webhooks
Webhooks send an HTTP POST to your URL when an event happens in your workspace.
Events
| Event | Triggered when |
|---|---|
contact.created / contact.updated / contact.deleted | A contact changes |
campaign.started / campaign.paused / campaign.completed | A campaign's status changes |
email.sent / email.delivered | An email is sent or delivered |
email.opened / email.clicked | A recipient opens an email or clicks a link in it |
email.bounced / email.complained / email.unsubscribed | A delivery fails, a recipient complains, or a recipient unsubscribes |
Payload
POST /your-endpoint HTTP/1.1
Content-Type: application/json
X-Beeving-Event: email.opened
X-Beeving-Signature: 5d41402abc4b2a76b9719d911017c592...{
"event": "email.opened",
"workspace_id": 4,
"data": { /* event-specific */ }
}Verify the signature
X-Beeving-Signature is the hex-encoded HMAC-SHA256 of the raw request body, keyed with your webhook's secret (whsec_…). Always verify it before you trust a payload.
import crypto from 'node:crypto'
function isValid(rawBody, signature, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
}$expected = hash_hmac('sha256', $request->getContent(), $secret);
$valid = hash_equals($expected, $request->header('X-Beeving-Signature'));Delivery
- Reply with a
2xxstatus within 10 seconds. - After 10 consecutive failures, the webhook is automatically disabled (
is_active: false). - Delivery logs are kept for 30 days.
The webhook object
{
"id": 1,
"url": "https://example.com/hooks/beeving",
"events": ["email.opened", "email.clicked"],
"is_active": true,
"last_triggered_at": "...",
"failure_count": 0,
"created_at": "...",
"updated_at": "..."
}The secret is returned only when you create the webhook and when you regenerate its secret.
Endpoints
| Method | Path | Permission | Description |
|---|---|---|---|
| GET | /webhooks | webhooks:read | List webhooks (paginated) |
| POST | /webhooks | webhooks:write | Create a webhook |
| GET | /webhooks/{id} | webhooks:read | Get a webhook |
| PUT | /webhooks/{id} | webhooks:write | Update a webhook (partial) |
| DELETE | /webhooks/{id} | webhooks:write | Delete a webhook. Returns 204. |
| POST | /webhooks/{id}/test | webhooks:write | Send a test event |
| GET | /webhooks/{id}/logs | webhooks:read | Delivery logs (paginated) |
| POST | /webhooks/{id}/regenerate-secret | webhooks:write | Rotate the signing secret |
List webhooks
GET /webhooksReturns webhooks from the token workspace. Results are ordered by created_at desc, then id desc, unless another sort is requested. The signing secret is never included.
| Query | Type | Description |
|---|---|---|
page | integer | Page number, minimum 1, default 1. |
per_page | integer | Webhooks per page, from 1 to 100, default 15. |
search | string | Partial URL search. Every term must match; at most six terms are used. |
is_active | boolean | 1 keeps active webhooks; 0 keeps inactive webhooks. |
event | enum | Keep webhooks subscribed to one supported event. |
sort | enum | created_at, updated_at, url, is_active, last_triggered_at, or failure_count. |
direction | enum | asc or desc. Default desc. |
The response contains data, navigation URLs in links, and meta with current_page, last_page, per_page, total, and count for the current page.
Create a webhook
{
"url": "https://example.com/hooks/beeving",
"events": ["email.opened", "email.clicked"],
"is_active": true
}| Field | Rules |
|---|---|
url | Required. A public HTTPS URL, max 255 characters |
events | Required. At least one event from the list above |
is_active | boolean, default true |
201: { "data": Webhook }. The response includes secret: store it now.
Test
POST /webhooks/{id}/test sends this payload:
{
"event": "webhook.test",
"workspace_id": 4,
"data": { "message": "This is a test webhook from Beeving", "timestamp": "2026-04-01T08:00:00+00:00" }
}The response reports { "success", "message", "status_code" }.
Logs
GET /webhooks/{id}/logsOnly logs belonging to the requested webhook in the token workspace are returned. Search matches event names only and never inspects stored payloads or responses.
| Query | Type | Description |
|---|---|---|
page | integer | Page number, minimum 1, default 1. |
per_page | integer | Logs per page, from 1 to 100, default 15. |
search | string | Partial event-name search. Every term must match; at most six terms are used. |
success | boolean | 1 keeps successful deliveries; 0 keeps failed deliveries. |
status_code | integer | Exact HTTP status, from 0 to 599. Status 0 means the endpoint was not reached. |
created_from | date | Include logs created on or after this YYYY-MM-DD date. |
created_to | date | Include logs created on or before this YYYY-MM-DD date. |
sort | enum | created_at, event, status_code, or success. |
direction | enum | asc or desc. Default desc. |
{
"data": [
{ "id": 55, "event": "email.opened", "payload": {}, "status_code": 200, "response": "ok", "success": true, "created_at": "..." }
],
"links": { "first": "...", "last": "...", "prev": null, "next": null },
"meta": { "current_page": 1, "last_page": 1, "per_page": 15, "total": 1, "count": 1 }
}Log resources expose only the recorded event, payload, delivery status, truncated response and timestamp. They never include the webhook signing secret.
Regenerate the secret
200: { "data": { "id": 1, "secret": "whsec_…" }, "message": "Secret regenerated successfully" }
The old secret stops working immediately.