Home/Blog/Zendesk Webhooks: Complete Guide with Payload Examples [2025]
Developer Tools

Zendesk Webhooks: Complete Guide with Payload Examples [2025]

Complete guide to Zendesk webhooks with setup instructions, payload examples, signature verification, and implementation code. Learn how to integrate Zendesk webhooks into your application with step-by-step tutorials for ticket automation and customer support workflows.

By Inventive HQ Team
Zendesk Webhooks: Complete Guide with Payload Examples [2025]

When a customer submits a high-priority support ticket at 2 AM, you need to know immediately—not when you check your dashboard the next morning. Zendesk webhooks solve this problem by sending real-time HTTP notifications to your server the moment events occur, enabling you to automate escalations, sync data with external systems, trigger custom workflows, and provide instant customer support responses.

Whether you're building automated ticket routing, integrating Zendesk with your CRM, creating custom analytics dashboards, or triggering notifications in Slack when tickets are created, Zendesk webhooks provide the foundation for powerful customer support automation.

In this comprehensive guide, you'll learn:

  • How to set up Zendesk webhooks in your admin dashboard
  • Complete webhook event types and payload structures
  • Step-by-step signature verification with HMAC-SHA256
  • Production-ready code examples in Node.js, Python, and PHP
  • Best practices for handling retries, timeouts, and the circuit breaker
  • Testing strategies and troubleshooting common issues

Before we dive in, you can test Zendesk webhooks without any setup using our Webhook Payload Generator—perfect for validating your signature verification logic and testing event handlers locally.

What Are Zendesk Webhooks?

Zendesk webhooks are HTTP callbacks that Zendesk sends to your application when specific events occur in your Zendesk account. Instead of continuously polling the Zendesk API to check for changes, webhooks push notifications to your server in real-time, reducing latency and API calls.

How Zendesk Webhooks Work:

[Ticket Created] → [Zendesk Server] → [Your Webhook Endpoint] → [Your Application Logic]
     Event              Detects Event       Receives HTTP POST         Processes Event

When you configure a webhook in Zendesk, you specify:

  • Endpoint URL: Where Zendesk sends the HTTP request
  • Event Types: Which activities trigger the webhook (tickets, users, organizations)
  • Authentication: How to secure the webhook (signature verification)
  • Request Format: JSON payload structure

Benefits of Zendesk Webhooks:

  • Real-time notifications: Receive events instantly (no polling delay)
  • Reduced API usage: No need to continuously query for changes
  • Scalable automation: Build complex workflows triggered by support events
  • Multi-system integration: Sync Zendesk data with CRM, databases, analytics platforms
  • Custom business logic: Implement rules beyond Zendesk's built-in automation

Prerequisites:

  • Active Zendesk account (Support, Guide, or Messaging)
  • Admin access to configure webhooks
  • HTTPS endpoint to receive webhook requests
  • Ability to verify HMAC-SHA256 signatures

Zendesk supports webhooks across multiple products including Support (tickets), Guide (knowledge base articles), Messaging (conversations), and more—making it a versatile integration point for your customer support stack.

Setting Up Zendesk Webhooks

Setting up Zendesk webhooks requires admin access to your Zendesk account. Follow these steps to create your first webhook:

Step 1: Access Webhook Settings

  1. Log in to your Zendesk account as an administrator
  2. Click the Admin icon (gear symbol) in the left sidebar
  3. Navigate to Apps and integrations > Webhooks
  4. Click Create webhook button in the top-right corner

Step 2: Configure Basic Settings

  1. Webhook Name: Enter a descriptive name (e.g., "Production Ticket Notifications")
  2. Description: Add optional details about the webhook's purpose
  3. Endpoint URL: Enter your webhook endpoint (must be HTTPS)
https://yourdomain.com/webhooks/zendesk
  1. Request Method: Select POST (recommended for most use cases)
  2. Request Format: Choose JSON (standard for webhook integrations)

Step 3: Configure Authentication

Zendesk provides three authentication options:

  • No authentication: Not recommended for production
  • Basic authentication: Username/password credentials
  • Bearer token: API token authentication (recommended)

For maximum security, select Bearer token and enter your API token in the Authorization header format.

Step 4: Select Event Types

Choose which Zendesk events will trigger your webhook:

  1. Click Connect to Zendesk events

  2. Select events from the dropdown menu:

    • Ticket events: ticket.created, ticket.updated, ticket.solved
    • User events: user.created, user.updated, user.deleted
    • Organization events: organization.created, organization.updated
    • Article events: article.published, article.updated
    • Messaging events: messaging conversation events
  3. You can select multiple event types for a single webhook

Note: For ticket-based automation with specific conditions, you'll need to connect your webhook to a Trigger or Automation (covered in the next section).

Step 5: Retrieve Webhook Secret

After creating the webhook:

  1. Find your webhook in the list
  2. Click the Actions menu (three dots)
  3. Select Reveal secret to display the signing secret
  4. Copy and store this secret securely—you'll need it for signature verification

Important: Treat this secret like a password. Never commit it to version control or expose it publicly.

Step 6: Test Your Webhook

Zendesk provides a built-in testing feature:

  1. Click the Actions menu next to your webhook
  2. Select Test webhook
  3. Choose an event type to test
  4. Click Send test
  5. Zendesk sends a sample payload to your endpoint

Check your endpoint logs to confirm receipt.

Connecting Webhooks to Triggers (Optional)

For advanced ticket automation, connect webhooks to Zendesk triggers:

  1. Navigate to Admin > Objects and rules > Business rules > Triggers
  2. Create a new trigger or edit an existing one
  3. Define conditions (e.g., "Ticket priority is High")
  4. In Actions, select Notify webhook and choose your webhook
  5. Zendesk will send webhook requests only when trigger conditions are met

Pro Tips

  • Use separate webhooks for development, staging, and production environments
  • Subscribe only to events you need to reduce processing overhead
  • Monitor webhook logs in the Zendesk admin panel to track delivery status
  • Test signature verification before deploying to production
  • Start with a test webhook using a tool like webhook.site to inspect payloads

Zendesk Webhook Events & Payloads

Zendesk webhooks support a wide range of events across multiple domains. Each webhook payload follows a consistent structure with domain-specific details.

Event Domain Overview

Event DomainDescriptionCommon Use Cases
Ticket eventsSupport ticket lifecycleAutomation, escalation, analytics
User eventsUser profile changesCRM sync, access control
Organization eventsOrganization updatesAccount management, billing
Article eventsHelp Center contentContent management, search indexing
Community post eventsCommunity activityModeration, engagement tracking
Agent availability eventsAgent status updatesRouting, capacity planning
Messaging eventsConversation activityChat automation, response tracking

Standard Payload Structure

All Zendesk webhook payloads share this top-level structure:

{
  "account_id": 123456,
  "id": "01HQGJ5H8F8Z7Y6X5W4V3U2T1",
  "type": "zen:event-type:domain.action",
  "subject": "zen:ticket:123456789",
  "time": "2025-01-24T15:30:00.000Z",
  "zendesk_event_version": "2022-06-20",
  "event": {
    "type": "change",
    "source": "api"
  },
  "detail": {
    // Domain-specific resource data
  }
}

Key Fields:

  • account_id - Your Zendesk account identifier
  • id - Unique event identifier (use for idempotency)
  • type - Event type in format zen:domain:action
  • subject - Resource reference (e.g., ticket ID)
  • time - ISO 8601 timestamp when event occurred
  • detail - Object containing the changed resource

Event: Ticket Created

Triggered when: A new support ticket is created through any channel (email, web form, API, chat)

Event Type: zen:event-type:ticket.created

Payload Example:

{
  "account_id": 123456,
  "id": "01HQGJ5H8F8Z7Y6X5W4V3U2T1",
  "type": "zen:event-type:ticket.created",
  "subject": "zen:ticket:987654",
  "time": "2025-01-24T15:30:00.000Z",
  "zendesk_event_version": "2022-06-20",
  "event": {
    "type": "change",
    "source": "web"
  },
  "detail": {
    "id": 987654,
    "external_id": null,
    "type": "incident",
    "subject": "Unable to access account",
    "description": "I cannot log in to my account. Getting error 'Invalid credentials'.",
    "priority": "high",
    "status": "new",
    "recipient": "[email protected]",
    "requester_id": 445566,
    "submitter_id": 445566,
    "assignee_id": null,
    "organization_id": 778899,
    "group_id": 112233,
    "collaborator_ids": [],
    "tags": ["login", "urgent"],
    "created_at": "2025-01-24T15:30:00Z",
    "updated_at": "2025-01-24T15:30:00Z",
    "due_at": null,
    "via": {
      "channel": "web"
    }
  }
}

Key Fields:

  • detail.id - Unique ticket identifier
  • detail.subject - Ticket title
  • detail.priority - Ticket priority (low, normal, high, urgent)
  • detail.status - Current status (new, open, pending, solved, closed)
  • detail.requester_id - Customer who submitted the ticket
  • detail.assignee_id - Agent assigned (null if unassigned)
  • detail.tags - Array of ticket tags

Use Cases: Trigger escalation workflows for high-priority tickets, create tickets in external systems, send instant notifications to on-call teams

Event: Ticket Updated

Triggered when: Any ticket field changes (status, assignee, priority, custom fields, etc.)

Event Type: zen:event-type:ticket.updated

Payload Example:

{
  "account_id": 123456,
  "id": "01HQGJ5H8F8Z7Y6X5W4V3U2T2",
  "type": "zen:event-type:ticket.updated",
  "subject": "zen:ticket:987654",
  "time": "2025-01-24T16:15:00.000Z",
  "zendesk_event_version": "2022-06-20",
  "event": {
    "type": "change",
    "source": "api"
  },
  "detail": {
    "id": 987654,
    "subject": "Unable to access account",
    "priority": "urgent",
    "status": "open",
    "assignee_id": 334455,
    "updated_at": "2025-01-24T16:15:00Z",
    "current": {
      "status": "open",
      "assignee_id": 334455,
      "priority": "urgent"
    },
    "previous": {
      "status": "new",
      "assignee_id": null,
      "priority": "high"
    }
  }
}

Key Fields:

  • detail.current - New field values after update
  • detail.previous - Field values before update
  • detail.assignee_id - Newly assigned agent

Use Cases: Track SLA compliance, notify customers of status changes, sync ticket data to external databases, monitor agent workload

Event: Ticket Comment Added

Triggered when: A public or private comment is added to a ticket

Event Type: zen:event-type:ticket.comment_added

Payload Example:

{
  "account_id": 123456,
  "id": "01HQGJ5H8F8Z7Y6X5W4V3U2T3",
  "type": "zen:event-type:ticket.comment_added",
  "subject": "zen:ticket:987654",
  "time": "2025-01-24T16:30:00.000Z",
  "zendesk_event_version": "2022-06-20",
  "event": {
    "type": "change",
    "source": "web"
  },
  "detail": {
    "ticket_id": 987654,
    "comment": {
      "id": 556677,
      "type": "Comment",
      "body": "I've reset your password. Please check your email for the reset link.",
      "html_body": "<p>I've reset your password. Please check your email for the reset link.</p>",
      "public": true,
      "author_id": 334455,
      "attachments": [],
      "created_at": "2025-01-24T16:30:00Z"
    }
  }
}

Key Fields:

  • detail.comment.body - Plain text comment content
  • detail.comment.public - Whether comment is visible to requester
  • detail.comment.author_id - User who added the comment
  • detail.comment.attachments - Array of attachment objects

Use Cases: Trigger AI sentiment analysis on customer responses, archive support conversations, create transcripts for compliance, notify stakeholders of internal notes

Event: Ticket Solved

Triggered when: A ticket status changes to "solved"

Event Type: zen:event-type:ticket.solved

Payload Example:

{
  "account_id": 123456,
  "id": "01HQGJ5H8F8Z7Y6X5W4V3U2T4",
  "type": "zen:event-type:ticket.solved",
  "subject": "zen:ticket:987654",
  "time": "2025-01-24T17:00:00.000Z",
  "zendesk_event_version": "2022-06-20",
  "event": {
    "type": "change",
    "source": "web"
  },
  "detail": {
    "id": 987654,
    "status": "solved",
    "assignee_id": 334455,
    "resolution_time_minutes": 90,
    "satisfaction_rating": null,
    "solved_at": "2025-01-24T17:00:00Z"
  }
}

Key Fields:

  • detail.resolution_time_minutes - Time from creation to resolution
  • detail.solved_at - Timestamp when ticket was solved
  • detail.satisfaction_rating - Customer satisfaction score (if available)

Use Cases: Calculate CSAT scores, track resolution metrics, send customer feedback surveys, update SLA dashboards, trigger post-resolution workflows

Event: User Created

Triggered when: A new user (customer or agent) is created in Zendesk

Event Type: zen:event-type:user.created

Payload Example:

{
  "account_id": 123456,
  "id": "01HQGJ5H8F8Z7Y6X5W4V3U2T5",
  "type": "zen:event-type:user.created",
  "subject": "zen:user:445577",
  "time": "2025-01-24T14:00:00.000Z",
  "zendesk_event_version": "2022-06-20",
  "event": {
    "type": "change",
    "source": "api"
  },
  "detail": {
    "id": 445577,
    "external_id": "crm_user_123",
    "name": "Jane Smith",
    "email": "[email protected]",
    "role": "end-user",
    "organization_id": 778899,
    "phone": "+1-555-123-4567",
    "time_zone": "America/New_York",
    "locale": "en-US",
    "created_at": "2025-01-24T14:00:00Z",
    "tags": ["premium", "vip"]
  }
}

Key Fields:

  • detail.id - Unique user identifier
  • detail.email - User email address
  • detail.role - User type (end-user, agent, admin)
  • detail.organization_id - Associated organization
  • detail.external_id - Your system's user identifier

Use Cases: Sync new customers to CRM, provision access to other systems, trigger onboarding workflows, update customer databases

Webhook Signature Verification

Signature verification is critical for webhook security. Without it, anyone can send fake webhook requests to your endpoint, potentially manipulating your data or triggering unauthorized actions.

Why Signature Verification Matters

Without verification:

  • Attackers can spoof webhook requests
  • Malicious actors can trigger false events
  • Your application processes untrusted data
  • No guarantee requests came from Zendesk

With verification:

  • Cryptographic proof of authenticity
  • Protection against spoofing attacks
  • Confidence in data integrity
  • Mitigation of replay attacks (when combined with timestamp validation)

Zendesk's Signature Method

Zendesk uses HMAC-SHA256 for webhook signatures:

  • Algorithm: HMAC-SHA256
  • Encoding: Base64
  • Signature Header: X-Zendesk-Webhook-Signature
  • Timestamp Header: X-Zendesk-Webhook-Signature-Timestamp
  • Signing String: TIMESTAMP + BODY (concatenated)

Formula:

signature = base64(HMAC-SHA256(timestamp + body, webhook_secret))

Step-by-Step Verification Process

  1. Extract signature and timestamp from request headers
  2. Retrieve webhook secret from environment variables
  3. Concatenate timestamp + body (in that order)
  4. Compute expected signature using HMAC-SHA256
  5. Encode to base64
  6. Compare signatures using constant-time comparison
  7. Validate timestamp (optional but recommended)

Code Examples

Node.js / Express

const crypto = require('crypto');
const express = require('express');
const app = express();

// IMPORTANT: Use raw body for signature verification
app.use('/webhooks/zendesk', express.raw({type: 'application/json'}));

app.post('/webhooks/zendesk', (req, res) => {
  const signature = req.headers['x-zendesk-webhook-signature'];
  const timestamp = req.headers['x-zendesk-webhook-signature-timestamp'];
  const secret = process.env.ZENDESK_WEBHOOK_SECRET;

  // Validate headers exist
  if (!signature || !timestamp) {
    console.error('Missing signature or timestamp headers');
    return res.status(401).send('Unauthorized');
  }

  // Compute expected signature
  const signedPayload = timestamp + req.body.toString();
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('base64');

  // Verify signature using timing-safe comparison
  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )) {
    console.error('Invalid signature');
    return res.status(401).send('Unauthorized');
  }

  // Optional: Validate timestamp (prevent replay attacks)
  const requestTime = parseInt(timestamp);
  const currentTime = Date.now();
  const fiveMinutes = 5 * 60 * 1000;

  if (currentTime - requestTime > fiveMinutes) {
    console.error('Timestamp too old');
    return res.status(401).send('Request expired');
  }

  // Parse payload after verification
  const payload = JSON.parse(req.body.toString());

  // Process webhook
  console.log(`Received ${payload.type} event: ${payload.id}`);

  // Return 200 immediately
  res.status(200).send('Webhook received');

  // Process async
  processZendeskWebhook(payload);
});

async function processZendeskWebhook(payload) {
  // Your business logic here
  console.log('Processing webhook:', payload);
}

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Zendesk webhook server listening on port ${PORT}`);
});

Python / Flask

import hmac
import hashlib
import base64
import time
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = 'your_webhook_secret_here'

@app.route('/webhooks/zendesk', methods=['POST'])
def zendesk_webhook():
    # Get signature and timestamp from headers
    signature = request.headers.get('X-Zendesk-Webhook-Signature')
    timestamp = request.headers.get('X-Zendesk-Webhook-Signature-Timestamp')

    # Validate headers exist
    if not signature or not timestamp:
        return jsonify({'error': 'Missing signature or timestamp'}), 401

    # Get raw body
    payload = request.get_data()

    # Compute expected signature
    signed_payload = timestamp + payload.decode('utf-8')
    expected_signature = base64.b64encode(
        hmac.new(
            WEBHOOK_SECRET.encode('utf-8'),
            signed_payload.encode('utf-8'),
            hashlib.sha256
        ).digest()
    ).decode('utf-8')

    # Verify signature using constant-time comparison
    if not hmac.compare_digest(signature, expected_signature):
        return jsonify({'error': 'Invalid signature'}), 401

    # Optional: Validate timestamp (prevent replay attacks)
    request_time = int(timestamp)
    current_time = int(time.time() * 1000)  # Convert to milliseconds
    five_minutes = 5 * 60 * 1000

    if current_time - request_time > five_minutes:
        return jsonify({'error': 'Request expired'}), 401

    # Parse payload
    data = request.get_json()

    # Process webhook
    print(f"Received {data['type']} event: {data['id']}")

    # Return 200 immediately
    return jsonify({'received': True}), 200

if __name__ == '__main__':
    app.run(port=3000)

PHP

<?php
$secret = getenv('ZENDESK_WEBHOOK_SECRET');
$signature = $_SERVER['HTTP_X_ZENDESK_WEBHOOK_SIGNATURE'];
$timestamp = $_SERVER['HTTP_X_ZENDESK_WEBHOOK_SIGNATURE_TIMESTAMP'];

// Validate headers exist
if (!$signature || !$timestamp) {
    http_response_code(401);
    die('Missing signature or timestamp');
}

// Get raw POST body
$payload = file_get_contents('php://input');

// Compute expected signature
$signedPayload = $timestamp . $payload;
$expectedSignature = base64_encode(
    hash_hmac('sha256', $signedPayload, $secret, true)
);

// Verify signature using constant-time comparison
if (!hash_equals($signature, $expectedSignature)) {
    http_response_code(401);
    die('Invalid signature');
}

// Optional: Validate timestamp (prevent replay attacks)
$requestTime = intval($timestamp);
$currentTime = time() * 1000; // Convert to milliseconds
$fiveMinutes = 5 * 60 * 1000;

if ($currentTime - $requestTime > $fiveMinutes) {
    http_response_code(401);
    die('Request expired');
}

// Parse payload
$data = json_decode($payload, true);

// Process webhook
error_log("Received {$data['type']} event: {$data['id']}");

// Return 200 immediately
http_response_code(200);
echo 'Webhook received';

// Process async (consider using a queue)
processZendeskWebhook($data);

function processZendeskWebhook($data) {
    // Your business logic here
    error_log("Processing webhook: " . json_encode($data));
}
?>

Common Verification Errors

  • Parsing JSON before verification: Body is modified, signature fails

    • Solution: Use raw body parser, verify before parsing
  • Using wrong secret: Test vs production secrets are different

    • Solution: Verify secret from Zendesk dashboard, check environment variables
  • Not using constant-time comparison: Vulnerable to timing attacks

    • Solution: Use crypto.timingSafeEqual(), hmac.compare_digest(), or hash_equals()
  • Incorrect concatenation order: Timestamp must come before body

    • Solution: Ensure timestamp + body, not body + timestamp
  • Forgetting base64 encoding: HMAC digest must be base64 encoded

    • Solution: Always apply base64 encoding after HMAC computation

Testing Signature Verification

Use Zendesk's test secret for initial testing:

dGhpc19zZWNyZXRfaXNfZm9yX3Rlc3Rpbmdfb25seQ==

This static secret is used during webhook creation before your actual secret is generated. You can also test with our Webhook Payload Generator which generates properly signed Zendesk payloads.

Testing Zendesk Webhooks

Testing webhooks locally presents challenges since Zendesk needs a publicly accessible HTTPS endpoint to send webhook requests.

Local Development Challenges

  • Zendesk cannot reach localhost or 127.0.0.1
  • Webhooks require publicly accessible URLs
  • HTTPS/SSL certificates are required
  • Firewall and NAT prevent external access

Solution 1: ngrok Tunnel

ngrok creates a secure tunnel from a public URL to your local development server.

Installation:

# macOS
brew install ngrok

# Windows/Linux
# Download from https://ngrok.com/download

Usage:

# Start your local webhook server
node server.js
# Server running on http://localhost:3000

# In another terminal, expose localhost via ngrok
ngrok http 3000

# ngrok displays a public URL:
# Forwarding: https://abc123.ngrok.io -> http://localhost:3000

Configure in Zendesk:

  1. Go to Zendesk admin > Webhooks
  2. Edit your webhook endpoint URL
  3. Enter the ngrok URL: https://abc123.ngrok.io/webhooks/zendesk
  4. Save and test

Benefits:

  • ✅ Secure HTTPS tunnel
  • ✅ Inspect requests in ngrok web interface
  • ✅ Replay requests for debugging

Limitations:

  • ❌ URL changes on restart (free tier)
  • ❌ Requires ngrok running during testing
  • ❌ External dependency

Solution 2: Webhook Payload Generator

For testing without tunneling, use our Webhook Payload Generator:

Steps:

  1. Visit Webhook Payload Generator
  2. Select "Zendesk" from the provider dropdown
  3. Choose event type (e.g., zen:event-type:ticket.created)
  4. Customize payload fields:
    • Ticket ID, subject, priority
    • Requester, assignee, status
    • Custom fields and tags
  5. Enter your webhook secret
  6. Click Generate Payload
  7. Copy the signed payload with headers
  8. Send to your local endpoint using curl or Postman

Example curl command:

curl -X POST http://localhost:3000/webhooks/zendesk \
  -H "Content-Type: application/json" \
  -H "X-Zendesk-Webhook-Signature: generated_signature_here" \
  -H "X-Zendesk-Webhook-Signature-Timestamp: 1706106000000" \
  -d '{"account_id":123456,"type":"zen:event-type:ticket.created",...}'

Benefits:

  • ✅ No tunneling required
  • ✅ Test signature verification logic
  • ✅ Customize any payload field
  • ✅ Test error handling with invalid data
  • ✅ Works offline

Solution 3: Zendesk Test Feature

Zendesk provides built-in webhook testing:

  1. Navigate to Admin > Webhooks
  2. Find your webhook in the list
  3. Click Actions menu (three dots)
  4. Select Test webhook
  5. Choose an event type to test
  6. Click Send test

Zendesk sends a sample event to your endpoint with a valid signature.

Benefits:

  • ✅ Official test data
  • ✅ Valid signatures
  • ✅ Tests actual Zendesk integration

Limitations:

  • ❌ Fixed payload structure
  • ❌ Cannot customize event data
  • ❌ Requires public endpoint

Testing Checklist

Before deploying to production, verify:

  • Signature verification passes for valid requests
  • Signature verification rejects invalid signatures
  • Endpoint returns 200 status within 10 seconds
  • Timestamp validation prevents replay attacks
  • Idempotent processing (handles duplicate events)
  • Error handling for malformed payloads
  • Async processing (doesn't block response)
  • Logging captures event IDs and types
  • Queue system handles processing failures
  • Database stores event IDs for deduplication

Monitoring Webhook Delivery

Zendesk provides webhook monitoring in the admin dashboard:

  1. Go to Admin > Webhooks
  2. Click on your webhook name
  3. View delivery logs showing:
    • Timestamp of each request
    • HTTP status code returned
    • Response time
    • Error messages (if any)
    • Retry attempts

Use these logs to debug delivery failures and monitor webhook health.

Implementation Example

Here's a complete, production-ready webhook endpoint implementation with all best practices.

Requirements

A robust Zendesk webhook endpoint must:

  • Respond within 10-12 seconds (Zendesk timeout)
  • Return 200 status code immediately
  • Process events asynchronously (avoid blocking)
  • Handle retries gracefully (idempotency)
  • Log all events for debugging
  • Implement signature verification
  • Use queues for reliable processing

Complete Node.js Example

const express = require('express');
const crypto = require('crypto');
const Queue = require('bull'); // npm install bull redis
const { PrismaClient } = require('@prisma/client');

const app = express();
const prisma = new PrismaClient();
const webhookQueue = new Queue('zendesk-webhooks', process.env.REDIS_URL);

// Environment variables
const WEBHOOK_SECRET = process.env.ZENDESK_WEBHOOK_SECRET;
const PORT = process.env.PORT || 3000;

// Parse raw body for signature verification
app.use('/webhooks/zendesk', express.raw({type: 'application/json'}));

// Zendesk webhook endpoint
app.post('/webhooks/zendesk', async (req, res) => {
  try {
    // 1. Extract headers
    const signature = req.headers['x-zendesk-webhook-signature'];
    const timestamp = req.headers['x-zendesk-webhook-signature-timestamp'];

    if (!signature || !timestamp) {
      console.error('Missing signature or timestamp headers');
      return res.status(401).json({ error: 'Missing authentication headers' });
    }

    // 2. Verify signature
    const signedPayload = timestamp + req.body.toString();
    const expectedSignature = crypto
      .createHmac('sha256', WEBHOOK_SECRET)
      .update(signedPayload)
      .digest('base64');

    if (!crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    )) {
      console.error('Invalid signature');
      return res.status(401).json({ error: 'Invalid signature' });
    }

    // 3. Validate timestamp (prevent replay attacks)
    const requestTime = parseInt(timestamp);
    const currentTime = Date.now();
    const fiveMinutes = 5 * 60 * 1000;

    if (currentTime - requestTime > fiveMinutes) {
      console.error('Timestamp too old');
      return res.status(401).json({ error: 'Request expired' });
    }

    // 4. Parse payload
    const payload = JSON.parse(req.body.toString());
    const eventId = payload.id;
    const eventType = payload.type;

    // 5. Check for duplicate (idempotency)
    const exists = await prisma.webhookEvent.findUnique({
      where: { eventId }
    });

    if (exists) {
      console.log(`Event ${eventId} already processed, skipping`);
      return res.status(200).json({ received: true, duplicate: true });
    }

    // 6. Store event immediately (mark as received)
    await prisma.webhookEvent.create({
      data: {
        eventId,
        eventType,
        payload: payload,
        status: 'received',
        receivedAt: new Date()
      }
    });

    // 7. Queue for async processing
    await webhookQueue.add({
      eventId,
      eventType,
      payload
    }, {
      attempts: 3,
      backoff: {
        type: 'exponential',
        delay: 2000
      }
    });

    // 8. Return 200 immediately (within timeout)
    res.status(200).json({ received: true });

    // Logging
    console.log(`Queued ${eventType} event: ${eventId}`);

  } catch (error) {
    console.error('Webhook processing error:', error);
    // Still return 200 to prevent Zendesk retries for our errors
    res.status(200).json({ received: true, error: true });
  }
});

// Process webhooks from queue
webhookQueue.process(async (job) => {
  const { eventId, eventType, payload } = job.data;

  try {
    // Mark as processing
    await prisma.webhookEvent.update({
      where: { eventId },
      data: { status: 'processing', processingAt: new Date() }
    });

    // Handle different event types
    switch (eventType) {
      case 'zen:event-type:ticket.created':
        await handleTicketCreated(payload);
        break;

      case 'zen:event-type:ticket.updated':
        await handleTicketUpdated(payload);
        break;

      case 'zen:event-type:ticket.solved':
        await handleTicketSolved(payload);
        break;

      case 'zen:event-type:user.created':
        await handleUserCreated(payload);
        break;

      default:
        console.warn(`Unknown event type: ${eventType}`);
    }

    // Mark as completed
    await prisma.webhookEvent.update({
      where: { eventId },
      data: { status: 'completed', completedAt: new Date() }
    });

    console.log(`Successfully processed event ${eventId}`);

  } catch (error) {
    console.error(`Failed to process event ${eventId}:`, error);

    await prisma.webhookEvent.update({
      where: { eventId },
      data: {
        status: 'failed',
        error: error.message,
        failedAt: new Date()
      }
    });

    throw error; // Will trigger queue retry
  }
});

// Business logic handlers
async function handleTicketCreated(payload) {
  const ticket = payload.detail;

  console.log(`New ticket created: #${ticket.id} - ${ticket.subject}`);

  // Check if high priority
  if (ticket.priority === 'urgent' || ticket.priority === 'high') {
    // Send Slack notification
    await sendSlackAlert({
      channel: '#support-urgent',
      text: `🚨 High Priority Ticket: #${ticket.id}`,
      fields: [
        { title: 'Subject', value: ticket.subject },
        { title: 'Requester', value: ticket.requester_id },
        { title: 'Priority', value: ticket.priority }
      ]
    });

    // Create PagerDuty incident for urgent tickets
    if (ticket.priority === 'urgent') {
      await createPagerDutyIncident({
        title: `Urgent Zendesk Ticket: ${ticket.subject}`,
        ticketId: ticket.id,
        severity: 'high'
      });
    }
  }

  // Sync to external CRM
  await syncTicketToCRM(ticket);
}

async function handleTicketUpdated(payload) {
  const ticket = payload.detail;

  console.log(`Ticket updated: #${ticket.id}`);

  // Check if assignment changed
  if (ticket.current?.assignee_id !== ticket.previous?.assignee_id) {
    // Notify new assignee
    await notifyAgent(ticket.current.assignee_id, {
      message: `You've been assigned ticket #${ticket.id}: ${ticket.subject}`,
      ticketUrl: `https://yourcompany.zendesk.com/agent/tickets/${ticket.id}`
    });
  }

  // Update analytics dashboard
  await updateTicketMetrics(ticket);
}

async function handleTicketSolved(payload) {
  const ticket = payload.detail;

  console.log(`Ticket solved: #${ticket.id}`);

  // Send customer satisfaction survey
  await sendCSATSurvey({
    ticketId: ticket.id,
    requesterId: ticket.assignee_id,
    resolutionTime: ticket.resolution_time_minutes
  });

  // Update SLA dashboard
  await recordResolutionMetrics({
    ticketId: ticket.id,
    resolutionTime: ticket.resolution_time_minutes,
    assigneeId: ticket.assignee_id
  });
}

async function handleUserCreated(payload) {
  const user = payload.detail;

  console.log(`New user created: ${user.email}`);

  // Sync to CRM
  await createCRMContact({
    name: user.name,
    email: user.email,
    zendeskId: user.id,
    organizationId: user.organization_id
  });

  // Trigger onboarding workflow
  await triggerOnboarding(user.email);
}

// Helper functions (implement based on your needs)
async function sendSlackAlert(data) {
  // Slack webhook implementation
}

async function createPagerDutyIncident(data) {
  // PagerDuty API implementation
}

async function syncTicketToCRM(ticket) {
  // CRM sync implementation
}

async function notifyAgent(agentId, message) {
  // Agent notification implementation
}

async function updateTicketMetrics(ticket) {
  // Analytics implementation
}

async function sendCSATSurvey(data) {
  // Survey email implementation
}

async function recordResolutionMetrics(data) {
  // Metrics recording implementation
}

async function createCRMContact(user) {
  // CRM contact creation implementation
}

async function triggerOnboarding(email) {
  // Onboarding workflow implementation
}

// Health check endpoint
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok' });
});

// Start server
app.listen(PORT, () => {
  console.log(`Zendesk webhook server listening on port ${PORT}`);
  console.log(`Queue processing: ${webhookQueue.name}`);
});

// Graceful shutdown
process.on('SIGTERM', async () => {
  console.log('SIGTERM received, shutting down gracefully');
  await webhookQueue.close();
  await prisma.$disconnect();
  process.exit(0);
});

Database Schema (Prisma)

model WebhookEvent {
  id           String   @id @default(cuid())
  eventId      String   @unique
  eventType    String
  payload      Json
  status       String   // received, processing, completed, failed
  error        String?
  receivedAt   DateTime @default(now())
  processingAt DateTime?
  completedAt  DateTime?
  failedAt     DateTime?

  @@index([eventId])
  @@index([status])
  @@index([eventType])
}

Key Implementation Details

  1. Raw body parsing - Required for signature verification before JSON parsing
  2. Timing-safe comparison - Prevents timing attacks on signature verification
  3. Timestamp validation - Rejects requests older than 5 minutes (replay attack prevention)
  4. Idempotency check - Prevents duplicate processing using event IDs
  5. Queue-based processing - Responds fast (< 1 second), processes async
  6. Error handling - Graceful failures, still returns 200 to prevent Zendesk retries
  7. Comprehensive logging - Detailed logs for debugging and monitoring
  8. Retry logic - Queue automatically retries failed jobs with exponential backoff
  9. Database tracking - Stores all events with status tracking
  10. Graceful shutdown - Closes queue and database connections properly

Best Practices

Follow these best practices to build reliable, secure, and performant Zendesk webhook integrations.

Security

  • Always verify signatures - Never process unsigned webhooks
  • Use HTTPS endpoints only - Zendesk requires SSL/TLS
  • Store secrets securely - Use environment variables, never commit to code
  • Validate timestamps - Prevent replay attacks (5-10 minute window)
  • Rate limit endpoints - Protect against abuse
  • Implement authentication - Add bearer tokens for additional security layer
  • Monitor failed verifications - Alert on repeated signature failures (potential attack)
  • Rotate secrets periodically - Follow security policy for credential rotation

Performance

  • Respond within 10 seconds - Zendesk times out at 10-12 seconds
  • Return 200 immediately - Process async, don't block the response
  • Use queue systems - Redis (Bull/BullMQ), RabbitMQ, AWS SQS for async processing
  • Implement exponential backoff - For external API calls during processing
  • Monitor processing times - Track queue depth and processing duration
  • Scale workers - Add queue workers to handle increased load
  • Optimize database queries - Use indexes on event_id, status, created_at
  • Cache frequently accessed data - Reduce database load

Reliability

  • Implement idempotency - Track event IDs, process each event only once
  • Handle duplicate webhooks - Zendesk retries can cause duplicates
  • Implement retry logic - Retry failed processing with exponential backoff
  • Don't rely solely on webhooks - Run reconciliation jobs to catch missed events
  • Log all webhook events - Store raw payloads for debugging and replay
  • Monitor circuit breaker - Track error rates to avoid triggering Zendesk's circuit breaker
  • Implement fallbacks - Use Zendesk API polling as backup for critical workflows
  • Test failure scenarios - Verify your system handles Zendesk outages gracefully

Monitoring & Observability

  • Track webhook delivery success rate - Alert on drops below threshold
  • Alert on signature verification failures - Potential security issue
  • Monitor processing queue depth - Detect bottlenecks early
  • Log event IDs for traceability - Enable end-to-end tracking
  • Set up health checks - Monitor endpoint availability
  • Dashboard key metrics - Events received, processed, failed, avg processing time
  • Error categorization - Group errors by type for targeted fixes
  • SLA tracking - Measure time from webhook receipt to processing completion

Zendesk-Specific Best Practices

  • Use triggers for conditional webhooks - Filter events before sending (reduce load)
  • Subscribe only to needed events - Fewer events = less processing overhead
  • Understand circuit breaker limits - 70% failure rate or 1,000 errors in 5 minutes
  • Monitor webhook status - Check Zendesk admin panel regularly
  • Test in sandbox first - Verify integration before production deployment
  • Handle empty bodies - Some webhook requests (GET, DELETE) may have empty bodies
  • Use webhook invocation logs - Zendesk provides detailed delivery logs in admin UI
  • Coordinate with triggers - Design webhooks and triggers together for optimal automation

Code Quality

  • Use environment variables - All secrets and configuration should be external
  • Implement structured logging - JSON logs for easy parsing and searching
  • Write integration tests - Test signature verification, idempotency, error handling
  • Document event handlers - Clear comments explaining business logic
  • Version your webhook endpoints - Enable gradual rollouts and rollbacks
  • Add metrics - Instrument code with metrics for Prometheus/Datadog/CloudWatch

Development Workflow

  • Separate environments - Different webhooks for dev, staging, production
  • Use test webhooks - Leverage Zendesk's test feature during development
  • Validate with generator - Use our Webhook Payload Generator for testing
  • Monitor logs during deploys - Watch for errors after releasing changes
  • Implement feature flags - Control webhook processing behavior without redeployment

Common Issues & Troubleshooting

Issue 1: Signature Verification Failing

Symptoms:

  • 401 errors in Zendesk webhook delivery logs
  • "Invalid signature" or "Unauthorized" in application logs
  • Webhook marked as failing in Zendesk admin

Causes & Solutions:

Using wrong secretSolution: Verify secret in Zendesk admin (Actions > Reveal secret). Check environment variable matches exactly. Ensure test vs production secrets are correct.

Parsing JSON before verificationSolution: Use raw body parser (express.raw()), verify signature on raw bytes, then parse JSON. Never use express.json() on webhook endpoints.

Incorrect concatenation orderSolution: Must be timestamp + body, not body + timestamp. Verify concatenation order in your code.

Missing base64 encodingSolution: After HMAC-SHA256, encode to base64: digest('base64') in Node.js, base64.b64encode() in Python, base64_encode() in PHP.

Not using constant-time comparisonSolution: Use crypto.timingSafeEqual(), hmac.compare_digest(), or hash_equals() to prevent timing attacks.

Encoding issuesSolution: Ensure consistent UTF-8 encoding. Convert strings to bytes properly before hashing.


Issue 2: Webhook Timeouts

Symptoms:

  • "Failed: 504 Gateway Timeout" in Zendesk logs
  • Webhook marked as failed after 10-12 seconds
  • Retries triggered repeatedly

Causes & Solutions:

Slow database queries blocking responseSolution: Return 200 immediately, move database operations to async queue processing. Maximum response time should be < 1 second.

External API calls in webhook handlerSolution: Queue external API calls (CRM sync, notifications) for background processing. Never wait for third-party services in webhook handler.

Complex business logic taking too longSolution: Validate signature, store event, queue for processing, return 200. All business logic should happen async.

Cold start delays (serverless)Solution: Keep functions warm with scheduled pings, or use provisioned concurrency (Lambda). Consider non-serverless architecture for webhooks.


Issue 3: Circuit Breaker Triggered

Symptoms:

  • Webhook stops receiving events
  • "Circuit breaker activated" message in Zendesk
  • No webhook deliveries despite events occurring

Causes & Solutions:

High error rate (70% failures in 5 minutes)Solution: Fix underlying errors causing failures. Check application logs for exceptions. Ensure endpoint returns 200 for valid requests.

Over 1,000 errors in 5 minutesSolution: Reduce event volume by filtering events in triggers. Fix bugs causing errors. Scale infrastructure to handle load.

Endpoint unreachableSolution: Verify DNS, SSL certificate validity, firewall rules. Check server health and availability.

Recovery:

  • Fix the underlying issue
  • Zendesk automatically reactivates after cooldown period
  • Contact Zendesk support if circuit breaker doesn't reset

Issue 4: Duplicate Events Processed

Symptoms:

  • Same event processed multiple times
  • Duplicate records in database
  • Duplicate notifications sent

Causes & Solutions:

No idempotency checkSolution: Store event IDs in database before processing. Check if event ID exists before processing. Use unique constraints on event_id column.

Race condition in duplicate checkSolution: Use database transactions or atomic operations. Consider using Redis SET with NX (set if not exists) for distributed systems.

Network retries causing duplicatesSolution: Design operations to be idempotent. Use upserts instead of inserts. Check resource existence before creating.

Example idempotency check:

const exists = await db.webhookEvents.findUnique({
  where: { eventId: payload.id }
});

if (exists) {
  console.log('Duplicate event, skipping');
  return res.status(200).json({ duplicate: true });
}

Issue 5: Missing Webhooks

Symptoms:

  • Expected webhooks not arriving
  • Events occurring in Zendesk but no webhook received
  • Delivery logs show success but endpoint not hit

Causes & Solutions:

Firewall blocking Zendesk IPsSolution: Whitelist Zendesk IP ranges in firewall. Check cloud provider security groups (AWS, GCP, Azure).

Wrong endpoint URL configuredSolution: Verify webhook URL in Zendesk admin matches deployed endpoint. Check for typos, missing paths, wrong domain.

SSL certificate issuesSolution: Ensure valid SSL certificate (not self-signed). Check certificate expiration. Verify full certificate chain is installed.

Webhook not connected to triggerSolution: For ticket events, webhook must be connected to a trigger or automation in Zendesk. Check trigger conditions are met.

Event type not subscribedSolution: Verify webhook subscribes to the event type. Check event configuration in webhook settings.


Issue 6: Zendesk Test Secret Not Working

Symptoms:

  • Signature verification fails during webhook creation
  • Test webhooks rejected with 401

Causes & Solutions:

Using production secret for testSolution: Use test secret dGhpc19zZWNyZXRfaXNfZm9yX3Rlc3Rpbmdfb25seQ== during webhook creation. Switch to real secret after creation.

Signature verification too strictSolution: Temporarily allow test secret in code during webhook setup, remove after configuration complete.


Debugging Checklist

When troubleshooting Zendesk webhook issues:

  • Check Zendesk webhook delivery logs (Admin > Webhooks > Webhook name)
  • Verify webhook endpoint is publicly accessible (test with curl)
  • Test signature verification with known-good payload
  • Check application logs for errors and exceptions
  • Verify SSL certificate is valid and not expired
  • Test with Webhook Payload Generator
  • Check firewall rules and security groups
  • Verify environment variables are set correctly
  • Monitor queue depth and worker status
  • Check database for failed events
  • Verify webhook connected to trigger (for ticket events)
  • Review Zendesk API rate limits and account quotas

Getting Help

If you're still experiencing issues:

  1. Check Zendesk Status: Visit status.zendesk.com for service incidents
  2. Review Documentation: developer.zendesk.com
  3. Test with Generator: Use our Webhook Payload Generator to isolate issues
  4. Contact Support: Zendesk provides developer support for webhook issues
  5. Community Forums: Search or post on Zendesk Developer Community

Frequently Asked Questions

Q: How often does Zendesk send webhooks? A: Zendesk sends webhooks immediately when events occur, typically within milliseconds to a few seconds. If delivery fails, Zendesk retries up to 3-5 times depending on the error code. For specific HTTP status codes (409, 429, 503), Zendesk implements automatic retry logic.

Q: Can I receive webhooks for past events? A: No, Zendesk webhooks only send notifications for events that occur after the webhook is created and activated. For historical data, use the Zendesk REST API to fetch past tickets, users, articles, and other resources. Webhooks are designed for real-time notifications, not historical data retrieval.

Q: What happens if my endpoint is down? A: Zendesk will retry failed deliveries up to 3-5 times with automatic retry logic. Requests timeout after 10-12 seconds. If 70% of requests fail within a 5-minute period, or if you receive 1,000+ errors in 5 minutes, Zendesk's circuit breaker will trigger and stop sending webhooks. Fix the issue and the circuit breaker will automatically reset after a cooldown period.

Q: Do I need different endpoints for test and production? A: Yes, it's highly recommended to use separate webhook URLs for development, staging, and production environments. Each webhook has its own signing secret, allowing you to test changes without affecting production systems. Use different Zendesk instances (sandbox and production) when available.

Q: How do I handle webhook ordering? A: Zendesk does not guarantee webhook delivery order, especially during retries or high load. Always use timestamp fields (time, created_at, updated_at) to determine event sequence. Implement idempotent operations that handle events correctly regardless of arrival order. Store events with timestamps for proper chronological processing.

Q: Can I filter which events I receive? A: Yes, when setting up your webhook, you select specific event types to subscribe to. Only subscribe to events you need to reduce processing overhead. For more advanced filtering based on conditions (e.g., only high-priority tickets), connect your webhook to a Zendesk trigger with custom conditions that determine when webhook requests are sent.

Q: How do webhooks differ from triggers in Zendesk? A: Triggers are Zendesk's internal automation rules that define when and how to take actions based on ticket conditions. Webhooks are the HTTP endpoints that receive event notifications. Triggers can invoke webhooks, adding conditional logic (e.g., "only send webhook when ticket priority is urgent"). Webhooks alone send all events of subscribed types without filtering.

Q: Are there rate limits for Zendesk webhooks? A: Yes, trial accounts are limited to 10 webhooks maximum with 60 invocations per minute. Production accounts have higher limits. Additionally, Zendesk's circuit breaker triggers if 70% of requests fail within 5 minutes or if you receive 1,000+ errors in 5 minutes (requires minimum 100 requests to trigger).

Q: Can I use Zendesk webhooks for real-time chat/messaging? A: Yes, Zendesk supports messaging event webhooks for real-time chat and messaging conversations. Subscribe to messaging events to receive notifications when messages are sent, conversations are created, or chat sessions start. This enables integration with custom chat interfaces and notification systems.

Q: How long does Zendesk store webhook delivery logs? A: Zendesk stores webhook delivery logs for approximately 7 days in the admin interface. You can view request timestamps, HTTP status codes, response times, and error messages. For long-term storage and analysis, implement logging in your webhook endpoint and store events in your database.

Next Steps & Resources

Now that you understand Zendesk webhooks, here's how to move forward:

Try It Yourself

  1. Set up a Zendesk webhook following the step-by-step guide above
  2. Test locally with our Webhook Payload Generator
  3. Implement signature verification using the code examples
  4. Deploy to production with monitoring and error handling
  5. Connect to triggers for conditional automation

Additional Resources

Related Tools & Guides

Need Help?

Conclusion

Zendesk webhooks provide a powerful, real-time integration mechanism for customer support automation. By following this guide, you now know how to:

  • ✅ Set up Zendesk webhooks in your admin dashboard
  • ✅ Verify webhook signatures securely with HMAC-SHA256
  • ✅ Implement production-ready webhook endpoints with queuing
  • ✅ Handle common issues like timeouts, duplicates, and circuit breakers
  • ✅ Test webhooks effectively with generators and local tools

Remember the key principles:

  1. Always verify signatures - Essential for security and authenticity
  2. Respond within 10 seconds - Zendesk's timeout is 10-12 seconds
  3. Process asynchronously - Queue operations for reliability
  4. Implement idempotency - Handle duplicate events gracefully
  5. Monitor everything - Track delivery, processing, and errors

Zendesk webhooks enable sophisticated customer support workflows, from instant escalations and multi-system data sync to custom analytics and automated responses. Whether you're integrating with your CRM, building custom dashboards, or automating ticket routing, webhooks provide the real-time foundation your support stack needs.

Start building with Zendesk webhooks today, and use our Webhook Payload Generator to test your integration safely and efficiently.

Have questions or run into issues? Use our Webhook Payload Generator for testing, review the official Zendesk documentation, or contact us for integration assistance.

Need Expert IT & Security Guidance?

Our team is ready to help protect and optimize your business technology infrastructure.