Managing Webhooks
All endpoints use the base path below and require the Manager or Admin role in the project. Authenticate with your API token as described in the introduction.
/api/v2/projects/{projectId}/webhooks
| Method | Path | Description |
|---|---|---|
GET | / | List webhooks with their delivery status |
GET | /{webhookId} | Get one webhook |
POST | / | Create a webhook |
PUT | /{webhookId} | Replace a webhook's definition (also re-enables it) |
DELETE | /{webhookId} | Delete a webhook |
POST | /{webhookId}/enable | Re-enable an automatically disabled webhook |
POST | /test | Send a test call to an unsaved definition |
Definition
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | No | — | Display name, at most 128 characters. |
url | string | Yes | — | https URL that receives the calls. |
enabled | boolean | No | true | Disabled webhooks are kept but not called. |
events | string[] | Yes | — | Any of item.created, item.updated, item.deleted, user.added, user.roleChanged, user.removed. |
customData | any JSON | No | — | Sent back unchanged in every call, at most 4096 bytes. |
Create
POST /api/v2/projects/{projectId}/webhooks
curl
curl -X POST "https://api.deon.cloud/api/v2/projects/{projectId}/webhooks" \
-H "Authorization: Bearer <your-api-token>" \
-H "Content-Type: application/json" \
-d '{
"name": "CRM sync",
"url": "https://example.com/hooks/deon",
"events": ["item.created", "item.deleted"],
"customData": { "board": 42 }
}'
JavaScript
const response = await fetch(
`https://api.deon.cloud/api/v2/projects/${projectId}/webhooks`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'CRM sync',
url: 'https://example.com/hooks/deon',
events: ['item.created', 'item.deleted'],
customData: { board: 42 },
}),
},
);
const webhook = await response.json();
Response
{
"id": "9f1e2c3a-6b7d-4e8f-9a0b-1c2d3e4f5a6b",
"name": "CRM sync",
"url": "https://example.com/hooks/deon",
"enabled": true,
"events": ["item.created", "item.deleted"],
"customData": { "board": 42 },
"createdAt": "2026-09-23T10:15:00+00:00",
"status": { "consecutiveFailures": 0 }
}
Changes are applied asynchronously. A GET right after a POST, PUT or DELETE can still show the previous
state for a moment.
List
GET /api/v2/projects/{projectId}/webhooks
Returns { "elements": [ … ] } with one object per webhook, shaped like the create response. status contains:
| Property | Description |
|---|---|
lastAttemptAt | Time of the last delivery attempt. |
lastSuccessAt | Time of the last successful delivery. |
lastStatusCode | HTTP status of the last attempt, if any. |
lastError | Why the last attempt failed, if it did. |
consecutiveFailures | Failed deliveries in a row. |
disabledAt, disabledReason | Set when the webhook was disabled automatically. |
inactive explains why an enabled webhook is never called: "copiedFromOtherProject" for webhooks that were copied
from another project, "invalidDefinition" for definitions that no longer pass validation (fix them with an
update, or delete them).
Get
GET /api/v2/projects/{projectId}/webhooks/{webhookId}
Update
PUT /api/v2/projects/{projectId}/webhooks/{webhookId}
Send the full definition, as for create. Updating also clears an automatic disable.
Delete
DELETE /api/v2/projects/{projectId}/webhooks/{webhookId}
Returns the deleted webhook.
Enable
POST /api/v2/projects/{projectId}/webhooks/{webhookId}/enable
Clears an automatic disable and the failure counter.
Test
POST /api/v2/projects/{projectId}/webhooks/test
Send a definition as for create. It is not saved. A signed call with a single ping change is sent right away and
the result is returned:
{ "statusCode": 200, "durationMs": 87, "error": null }
The body of your endpoint's response is never returned. The endpoint allows 10 calls per minute and user; beyond
that it answers 503 with a Retry-After header.