Skip to main content

Webhooks

A webhook sends case events to your own systems as they happen: a CRM, a chat bot, a data warehouse. Each request is signed with a secret only you and the desk know, and failed deliveries are retried.

Webhooks are set up by admins under Settings → Webhooks, and belong to one organization: they get events for cases in that organization and every unit below it.

Creating a webhook​

  1. Under Settings → Webhooks, choose New webhook.
  2. Fill in the fields below.
  3. Choose Create webhook.
  4. Copy the signing secret (whsec_…) and store it with the receiving service.
FieldWhat to enter
NameFor you, like the system it feeds.
Endpoint URLWhere the desk posts. In production it must be https:// and resolve to a public address (not loopback, private, link-local or reserved ranges), and must not carry a user name or password. The address is checked again before every delivery.
OrganizationShown when there is more than one.
EventsAt least one (below).

[!IMPORTANT] The signing secret is shown once. Afterwards the desk only shows its first and last characters.

Events​

EventWhendata
case.createdA new case: from a form or portal, logged by an agent, or created through the API or an assistant. A held request counts when an agent accepts it.{}
case.customer_repliedThe requester wrote, on the case page or by email.message
case.agent_repliedAn agent replied to the requester.message
case.status_changedThe status changed.from, to
case.assignedThe assignee changed.assignee_id (null when unassigned)
case.ratedThe requester answered the satisfaction survey.score (1 to 3), label (bad, okay, great)
agreement.sentAn agreement was sent to a contact for signing.see below
agreement.signedAn agreement was signed, or recorded as signed.see below
agreement.declinedA contact declined an agreement.see below, and reason
pingSend test event on the webhook page.message

The agreement events aren't about a case: instead of case and data, the body has org_id and agreement, with id, title, status, account (id, name), signer (name, title, email), sha256, signed_at, valid_until and url.

Plugins that are on add events of their own, such as Software Development's dev.task.done; the webhook editor lists them under each plugin's name. Their payload is described in building a plugin.

[!NOTE] Events are sent after automations and service levels have run, so the case in the payload is the one after rules have routed, labelled or assigned it.

Payload​

Every request is a POST with a JSON body:

{
"id": "cm3x9k2a40001…",
"event_id": "cm3x9k2a40001…",
"event": "case.agent_replied",
"created_at": "2026-10-21T09:14:03.512Z",
"case": {
"ref": "ABC-123",
"subject": "Can't sign in",
"status": "pending",
"priority": "normal",
"labels": ["login"],
"source": "form",
"channel": null,
"locale": "en",
"unit": { "id": "…", "name": "Support" },
"contact": { "email": "[email protected]", "name": "Sam Taylor" },
"assignee": { "id": "…", "name": "Alex Kim", "email": "[email protected]" },
"fields": { "message": "I can't sign in since this morning." },
"props": { "erp_order": "SO-40112" },
"created_at": "2026-10-21T08:52:40.118Z",
"url": "https://support.example.com/app/cases/ABC-123"
},
"data": {
"message": {
"id": "…",
"body": "Hi Sam, we've reset your account …",
"author": { "id": "…", "name": "Alex Kim" },
"created_at": "2026-10-21T09:14:03.400Z"
}
}
}
FieldMeaning
idThe delivery's id; it's also in the X-Desk-Delivery header. Automatic retries of one delivery keep the same id.
event_idIdentifies the event, and stays the same when a delivery is sent again with Redeliver: it is the id of the event's first delivery. It's also in the X-Desk-Event-Id header.
case.urlOpens the case in the agent app (sign-in needed); it is not the requester's link.
case.sourceform, portal, agent or api.
case.channelHow a case logged by an agent reached them (phone, email, in_person, chat, other), otherwise null.
case.assigneenull when nobody is assigned.

A ping has no case.

[!TIP] Store the event ids you have handled and ignore an event you've seen, so a redelivery isn't processed twice.

Headers on every request:

HeaderValue
Content-Typeapplication/json
X-Desk-EventThe event, like case.created.
X-Desk-DeliveryThe delivery id.
X-Desk-Event-IdThe event id: the same on every delivery of the same event, redeliveries included.
X-Desk-TimestampUnix time in seconds when this attempt was sent.
X-Desk-Signaturesha256= and the HMAC-SHA256, in hex, of the timestamp, a dot and the raw body, with the signing secret.

Verifying requests​

Check the signature on the raw body, before parsing the JSON, and refuse old timestamps so a captured request can't be replayed. In Node with Express:

import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

const secret = process.env.DESK_WEBHOOK_SECRET; // whsec_…

function verify(req) {
const ts = req.get('X-Desk-Timestamp') ?? '';
const sig = req.get('X-Desk-Signature') ?? '';
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = 'sha256=' + createHmac('sha256', secret).update(ts + '.').update(req.body).digest('hex');
return sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

const app = express();
app.post('/hooks/desk', express.raw({ type: 'application/json' }), (req, res) => {
if (!verify(req)) return res.status(401).end();
const event = JSON.parse(req.body);
// Answer quickly; do slow work in the background.
res.status(204).end();
handle(event);
});

The same snippet is on the webhook page under Verifying requests.

Answering, retries and backoff​

First attempt → Delivered
First attempt → Retrying: another status, a timeout, a refused connection or a redirect
Retrying → Delivered: a later attempt gets a 2xx within 10 seconds
Retrying → Gave up: after six attempts in all
  • Any 2xx answer within 10 seconds counts as delivered. Redirects are not followed and count as a failure.
  • Anything else (another status, a timeout, a refused connection) is retried up to five times, waiting longer each time: about 30 seconds to a minute before the first retry, roughly doubling after that, so the last retry comes about half an hour after the first attempt at most. After six attempts in all, the delivery is given up.
  • Retries send the same body and delivery id, with a new timestamp and signature. Use the delivery id to ignore a delivery you have already handled.
  • Deliveries are not guaranteed to arrive in order. Use created_at and the case's current state rather than the order of arrival.

[!NOTE] A webhook that is turned off, or whose URL fails the address check at delivery time, gets no retries: the delivery fails right away with the reason.

The delivery log​

The webhook page lists deliveries, newest first, under Deliveries: the event, the answer's status code (or no answer), how long it took and when. Refresh reloads the list.

Open one to see the request body, the response (its first 1,000 characters) and the outcome, with the error:

OutcomeMeaning
DeliveredThe endpoint answered with a 2xx in time.
Attempt 2 failed; retryingAn attempt failed and another is planned.
Gave up after 6 attemptsEvery attempt failed; no more retries.

The badge next to the webhook's name shows the last real delivery (test events don't count): Healthy, Sending, Failing, No deliveries yet, or Off.

Testing and redelivering​

  • Send test event posts a ping to the endpoint, signed like any other delivery, so you can check the receiving side.
  • Redeliver on a delivery sends the same event and data again as a new delivery, with a new delivery id and created_at but the same event_id. Use it after fixing the receiving side.

Turning off, changing and deleting​

  • The switch at the top turns the webhook On or Off. While it is off, events are not sent and pending retries fail.
  • Change the name, URL or events under Settings and save.
  • Delete (in the menu at the top) stops deliveries and removes the log.

Rotating the secret​

In the menu at the top, Rotate secret creates a new signing secret and shows it once.

[!WARNING] The old secret stops working right away, so update the receiving service at the same moment. Deliveries that fail in between are retried with the new secret, or can be redelivered.

Every change to a webhook, and every rotation, is in the audit log.

Through the API​

Admins and tokens with the admin scope manage webhooks through the REST API under /api/v1/webhooks. See API tokens and /api/docs on your desk.

EndpointWhat
/api/v1/webhooksList, create (the answer carries the secret once), change, delete.
POST /api/v1/webhooks/{id}/testSend a test event.
POST /api/v1/webhooks/{id}/secretRotate the secret.
GET /api/v1/webhooks/{id}/deliveriesThe delivery log.
POST /api/v1/webhooks/deliveries/{id}/redeliverRedeliver a delivery.