Webhooks

Receive signed HTTP callbacks when tickets change, from triggers and automations, with retries and a delivery log.

Webhooks send an HTTP request to your endpoint when a trigger or automation fires. You create them under Admin Center, Integrations, Webhooks, then use them as an action in any trigger or automation. Request bodies can use placeholders and follow Zendesk-shaped payloads, so receivers built for Zendesk webhooks often work unchanged.

Verify the signature

Every request carries two headers:

HeaderValue
X-Desk-TimestampUnix time in seconds when the request was signed
X-Desk-Signaturebase64(HMAC-SHA256(signing secret, timestamp + "." + raw body))

Compute the signature over the raw request body, compare it in constant time, and reject requests whose timestamp is more than 5 minutes old.

import crypto from 'node:crypto';

export function verify(rawBody, headers, secret) {
  const ts = headers['x-desk-timestamp'];
  const sig = headers['x-desk-signature'];
  if (!ts || !sig || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('base64');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}
function verify(string $rawBody, string $ts, string $sig, string $secret): bool
{
    if (abs(time() - (int) $ts) > 300) {
        return false;
    }
    $expected = base64_encode(hash_hmac('sha256', $ts.'.'.$rawBody, $secret, true));

    return hash_equals($expected, $sig);
}
import base64, hashlib, hmac, time

def verify(raw_body: bytes, ts: str, sig: str, secret: str) -> bool:
    if not ts or not sig or abs(time.time() - int(ts)) > 300:
        return False
    mac = hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256)
    expected = base64.b64encode(mac.digest()).decode()
    return hmac.compare_digest(expected, sig)

Retries

Answer with any 2xx status within a few seconds. Other answers and timeouts are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours; after that the delivery is marked failed. Every attempt, with its status and response, is in the webhook’s delivery log.

Make your receiver idempotent: a delivery can arrive more than once.

Delivery log and manual retry

Admin Center, Integrations, Webhooks shows every delivery with its request, response, status and attempts. Filter it by webhook, event (ticket.created, trigger, automation, test and so on) and status. Replay on any delivery, including one that has failed for good, queues a fresh copy of the same payload, signed with the current secret; the copy is linked to the original. The same log is in the API: GET /api/webhook_deliveries?event_type=ticket.created&status=failed and POST /api/webhook_deliveries/{id}/replay.

Outbound addresses

Webhook requests come from the published egress IP addresses, so you can allow them in your firewall.