Webhooks: send DeskCrew events to your own systems
Webhooks push DeskCrew events to your own systems as they happen, so you can log tickets to a data warehouse, ping an internal channel, or trigger a workflow without polling the API.
Setting one up
In your dashboard, open Settings and add a webhook endpoint. You provide an HTTPS URL, choose which events you care about, and DeskCrew gives you a signing secret. Keep that secret: it is how you prove a request genuinely came from us.
Events you can subscribe to
| Event | Fires when |
|---|---|
ticket.created | A new ticket arrives, from any channel |
ticket.resolved | A ticket is marked resolved |
message.created | A new message is added to a ticket |
issue.created | Someone submits an idea or bug on your feedback board |
issue.status_changed | You move an issue between statuses |
issue.shipped | An issue is marked shipped |
changelog.published | You publish a changelog entry |
What a delivery looks like
Every delivery is a POST with a JSON body and three headers:
X-Desk-Event: ticket.created
X-Desk-Delivery-Id: 12345
X-Desk-Signature: t=1730284800,v1=<hex>
X-Desk-Delivery-Id is stable across retries of the same event, so use it to make your handler idempotent.
Verifying the signature
The signature is an HMAC-SHA256 over the timestamp and the raw request body joined by a full stop, the same scheme Stripe and Slack use:
signature = HMAC_SHA256(your_secret, "{t}.{raw_request_body}")
Verify against the raw body, before any JSON parsing. Re-serialising the JSON will change the bytes and the signature will not match.
Reject anything where t is more than 5 minutes old. That window is what stops a captured request being replayed at you later.
import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(secret, header, rawBody) {
const [tPart, vPart] = header.split(',')
const t = Number(tPart.split('=')[1])
const presented = vPart.split('=')[1]
if (Math.abs(Date.now() / 1000 - t) > 300) return false
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
return timingSafeEqual(Buffer.from(expected), Buffer.from(presented))
}
Compare in constant time, as above. A plain string comparison leaks timing information.
Retries
Respond with any 2xx status to acknowledge. Anything else, or a timeout, is treated as a failure and retried with exponential backoff: roughly 1 minute, then 5 minutes, 30 minutes, 2 hours, 12 hours, up to 8 attempts across about a day.
After 8 failed attempts a delivery is marked dead, and repeated failures will auto-disable the endpoint so a broken URL does not queue forever. Fix the endpoint and re-enable it in your dashboard.
Return your 2xx quickly and do the real work afterwards. If your handler takes longer than the send timeout, we count it as a failure and retry, and you will process the same event twice.
Security notes
Your endpoint must be a public HTTPS URL. Requests to private and internal addresses are blocked, so a webhook cannot be pointed at something inside our network or yours.
Always verify the signature. Without it, anyone who learns your URL can post whatever they like to it.
