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

EventFires when
ticket.createdA new ticket arrives, from any channel
ticket.resolvedA ticket is marked resolved
message.createdA new message is added to a ticket
issue.createdSomeone submits an idea or bug on your feedback board
issue.status_changedYou move an issue between statuses
issue.shippedAn issue is marked shipped
changelog.publishedYou 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.

Related articles