Skip to main content

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
MethodPathDescription
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}/enableRe-enable an automatically disabled webhook
POST/testSend a test call to an unsaved definition

Definition​

PropertyTypeRequiredDefaultDescription
namestringNo—Display name, at most 128 characters.
urlstringYes—https URL that receives the calls.
enabledbooleanNotrueDisabled webhooks are kept but not called.
eventsstring[]Yes—Any of item.created, item.updated, item.deleted, user.added, user.roleChanged, user.removed.
customDataany JSONNo—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:

PropertyDescription
lastAttemptAtTime of the last delivery attempt.
lastSuccessAtTime of the last successful delivery.
lastStatusCodeHTTP status of the last attempt, if any.
lastErrorWhy the last attempt failed, if it did.
consecutiveFailuresFailed deliveries in a row.
disabledAt, disabledReasonSet 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.