Back to Docs

Webhooks

Receive real-time notifications when tickets are created, updated, or closed. Build automations and keep your systems in sync.

Overview

Webhooks let you receive HTTP POST requests to your server when events happen in Dispatch Tickets. Instead of polling the API, your system gets notified instantly.

Real-time

Events delivered within seconds of occurring

Secure

HMAC signatures verify authenticity

Reliable

Automatic retries with exponential backoff

Common use cases:

  • Send Slack notifications when tickets are created
  • Sync tickets to your CRM or database
  • Trigger automations when status changes
  • Update customer-facing dashboards in real-time
  • Log ticket activity for analytics

Creating Webhooks

Via the Dashboard

Go to Settings → Brands → [Brand] → Webhooks → Create Webhook. Enter your endpoint URL and select which events to receive.

Via the API

Create a webhook
const webhook = await dispatch.webhooks.create('br_abc123', {
  url: 'https://your-server.com/webhooks/dispatch',
  events: ['ticket.created', 'ticket.updated', 'comment.created'],
  secret: 'whsec_your_signing_secret', // Optional, auto-generated if omitted
});

// Save webhook.secret securely - you'll need it to verify signatures
console.log(webhook.id);     // 'wh_xyz789'
console.log(webhook.secret); // 'whsec_...'

Webhook Configuration

FieldRequiredDescription
urlYesHTTPS endpoint to receive events
eventsYesArray of event types to subscribe to
secretNoSigning secret (auto-generated if omitted)
enabledNoWhether webhook is active (default: true)

Event Types

EventTriggered When
ticket.createdA new ticket is created (via API, email, or portal)
ticket.updatedTicket fields change (status, assignee, custom fields)
ticket.closedTicket status changes to closed
ticket.reopenedClosed ticket is reopened
comment.createdA comment is added to a ticket
customer.createdA new customer is created

Subscribe to * to receive all events:

events: ['*'] // All current and future event types

Payload Format

Webhook payloads are JSON with a consistent structure:

Webhook payload
{
  "id": "evt_abc123",           // Unique event ID
  "type": "ticket.created",     // Event type
  "createdAt": "2024-01-15T10:30:00Z",
  "brandId": "br_xyz789",       // Brand where event occurred
  "data": {
    // Event-specific payload
  }
}

ticket.created

ticket.created payload
{
  "id": "evt_abc123",
  "type": "ticket.created",
  "createdAt": "2024-01-15T10:30:00Z",
  "brandId": "br_xyz789",
  "data": {
    "ticket": {
      "id": "tkt_def456",
      "ticketNumber": "TKT-1042",
      "title": "Need help with my order",
      "status": "open",
      "priority": "normal",
      "source": "email",
      "customer": {
        "id": "cus_ghi789",
        "email": "[email protected]",
        "name": "Jane Doe"
      },
      "createdAt": "2024-01-15T10:30:00Z"
    }
  }
}

ticket.updated

ticket.updated payload
{
  "id": "evt_abc124",
  "type": "ticket.updated",
  "createdAt": "2024-01-15T11:00:00Z",
  "brandId": "br_xyz789",
  "data": {
    "ticket": {
      "id": "tkt_def456",
      // ... full ticket object
    },
    "changes": {
      "status": { "from": "open", "to": "pending" },
      "assigneeId": { "from": null, "to": "usr_abc123" }
    }
  }
}

comment.created

comment.created payload
{
  "id": "evt_abc125",
  "type": "comment.created",
  "createdAt": "2024-01-15T11:15:00Z",
  "brandId": "br_xyz789",
  "data": {
    "comment": {
      "id": "cmt_jkl012",
      "body": "Thanks for reaching out! Let me look into this.",
      "authorType": "STAFF",
      "authorName": "Support Team",
      "createdAt": "2024-01-15T11:15:00Z"
    },
    "ticket": {
      "id": "tkt_def456",
      "ticketNumber": "TKT-1042"
    }
  }
}

Signature Verification

Every webhook request includes a signature header to verify the request came from Dispatch Tickets. Always verify signatures to prevent spoofed requests.

Signature Header

X-Dispatch-Signature: t=1705312200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8f9

The header contains:

  • t - Unix timestamp when the signature was generated
  • v1 - HMAC-SHA256 signature of the payload

Verification Steps

  1. Extract the timestamp and signature from the header
  2. Construct the signed payload: ${timestamp}.${rawBody}
  3. Compute HMAC-SHA256 using your webhook secret
  4. Compare signatures using constant-time comparison
  5. Verify timestamp is within tolerance (e.g., 5 minutes)

Node.js / TypeScript

Verify webhook signature
import crypto from 'crypto';

function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string,
  tolerance = 300 // 5 minutes
): boolean {
  // Parse signature header
  const parts = signature.split(',');
  const timestamp = parseInt(parts.find(p => p.startsWith('t='))?.slice(2) || '0');
  const expectedSig = parts.find(p => p.startsWith('v1='))?.slice(3);

  if (!timestamp || !expectedSig) {
    return false;
  }

  // Check timestamp tolerance
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - timestamp) > tolerance) {
    return false; // Request too old or from the future
  }

  // Compute expected signature
  const signedPayload = `${timestamp}.${payload}`;
  const computedSig = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(expectedSig),
    Buffer.from(computedSig)
  );
}

// Usage in Express
app.post('/webhooks/dispatch', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-dispatch-signature'] as string;
  const payload = req.body.toString();

  if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET!)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(payload);
  // Process event...

  res.status(200).send('OK');
});

Using the SDK

SDK signature verification
import { verifyWebhookSignature } from '@dispatchtickets/sdk';

app.post('/webhooks/dispatch', express.raw({ type: 'application/json' }), (req, res) => {
  const isValid = verifyWebhookSignature(
    req.body.toString(),
    req.headers['x-dispatch-signature'],
    process.env.WEBHOOK_SECRET!
  );

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  // Process event...
});

Retry Behavior

If your endpoint returns a non-2xx status code or times out, we automatically retry delivery:

AttemptDelayTime Since First Attempt
1 (initial)Immediate0
21 minute1 minute
35 minutes6 minutes
430 minutes36 minutes
5 (final)2 hours~2.5 hours

After 5 failed attempts, the event is marked as failed. You can view failed deliveries in the dashboard and manually retry them.

Timeout: Your endpoint must respond within 30 seconds. For long-running processing, return 200 immediately and process asynchronously.

Best Practices

Return 200 quickly

Acknowledge receipt immediately, then process the event asynchronously. This prevents timeouts and duplicate deliveries.

Async processing pattern
app.post('/webhooks/dispatch', async (req, res) => {
  // Verify signature first
  if (!verifySignature(req)) {
    return res.status(401).send('Invalid signature');
  }

  // Acknowledge immediately
  res.status(200).send('OK');

  // Process asynchronously
  const event = JSON.parse(req.body);
  await queue.add('process-webhook', event);
});

Handle duplicates

The same event may be delivered multiple times (retries, network issues). Use the event id to deduplicate.

Deduplication
async function processWebhook(event) {
  // Check if we've already processed this event
  const exists = await db.webhookEvents.findUnique({
    where: { eventId: event.id }
  });

  if (exists) {
    return; // Already processed
  }

  // Process the event
  await handleEvent(event);

  // Mark as processed
  await db.webhookEvents.create({
    data: { eventId: event.id, processedAt: new Date() }
  });
}

Use HTTPS

Webhook endpoints must use HTTPS. We don't deliver to HTTP URLs.

Monitor failures

Set up alerts for webhook failures. The dashboard shows delivery status and allows manual retries.

Debugging

Webhook Logs

View recent deliveries in Settings → Brands → [Brand] → Webhooks → [Webhook] → Logs. Each log entry shows:

  • Event type and ID
  • Request payload
  • Response status code
  • Response body (truncated)
  • Delivery time

Test Webhooks Locally

Use a tool like ngrok to expose your local server:

Test with ngrok
# Start ngrok tunnel
ngrok http 3000

# Use the ngrok URL as your webhook endpoint
# https://abc123.ngrok.io/webhooks/dispatch

Send Test Events

Use the dashboard or API to send a test event to your webhook:

Send test event
await dispatch.webhooks.test('br_abc123', 'wh_xyz789', {
  type: 'ticket.created' // Event type to simulate
});