How to set up Zendesk messaging message delivered webhooks

Stevia Putri

Stanley Nicholas
Last edited February 20, 2026
Expert Verified
Webhooks turn your Zendesk into a real-time notification engine. Instead of polling for updates, webhooks push data to your systems the moment something happens. For messaging specifically, delivery webhooks let you track whether messages actually reach your customers. Not just handed off to WhatsApp or Twilio. Actually delivered.
This matters because "sent" and "delivered" are two different things. A message might leave Zendesk, fail at the carrier level, and you'd never know without delivery tracking. Or it might sit undelivered because a customer's phone is offline. Delivery webhooks give you visibility into the full message lifecycle.
In this guide, you'll learn how to set up Zendesk message delivery webhooks from scratch. We'll cover the three delivery event types, walk through configuration step by step, and provide working code you can adapt for your own implementation.
What you'll need
Before diving in, make sure you have:
- A Zendesk account with admin access (webhook configuration requires admin permissions)
- An HTTPS endpoint to receive webhook payloads (a development server works fine for testing)
- Basic familiarity with JSON and HTTP concepts
- Optionally: a tool like Postman or curl for testing
If you're building this for production, you'll also want to think through authentication, error handling, and idempotency. We'll touch on all of those.
Understanding Zendesk message delivery events
Zendesk's messaging platform sends three types of delivery events. Each tells you something different about where your message stands.
The three delivery event types
| Event Type | When It Fires | What It Tells You |
|---|---|---|
conversation:message:delivery:channel | Message handed to channel API | The channel (WhatsApp, Twilio, etc.) accepted the message |
conversation:message:delivery:user | Message reached user's device | The customer actually received the message |
conversation:message:delivery:failure | Delivery failed | Something went wrong; error details included |
Here's how the flow works. When you send a message, it first hits the channel's API. If that API accepts the message, you get a delivery:channel event. For some channels (like WhatsApp or SMS via Twilio), you might later get a delivery:user event confirming the message reached the device. If anything fails along the way, you get delivery:failure with specific error information.
The isFinalEvent flag tells you whether more events are coming. When isFinalEvent: true, this is the last webhook you'll receive for that message. When false, the channel supports user-level delivery confirmation, so expect a follow-up.
Channel support varies
Not all channels can confirm delivery to the user. Some only confirm they received the message from Zendesk. Here's the breakdown:
Channels with full delivery confirmation (channel + user):
- Twilio SMS
- MessageBird
- iOS SDK, Android SDK, Unity SDK, Web Widget
Channels with channel delivery only:
- Facebook Messenger
- LINE
- Telegram
- Viber
- X (Twitter)
- Apple Messages for Business
For channels without user confirmation, the delivery:channel event will have isFinalEvent: true. That's your signal that no further updates are coming.
Why this matters for your implementation
If you're building analytics on top of these webhooks, you'll want to account for this variation. A "delivered" metric means different things depending on the channel. For WhatsApp, it's device-level confirmation. For Facebook Messenger, it just means Facebook's API accepted the message.
Step 1: Create a webhook in Zendesk Admin Center
Let's walk through creating your first delivery webhook.
First, navigate to Admin Center > Apps and integrations > Webhooks. If you don't see the Webhooks option, check that your role has the proper permissions. Learn more in the Zendesk webhooks documentation.
Click Create webhook. You'll see a form with several fields:
Name: Give it something descriptive like "Message Delivery Tracker" or "Production Delivery Events."
Endpoint URL: This is where Zendesk will POST the webhook payloads. For development, you can use a tool like ngrok to expose a local server. For production, this should be an HTTPS URL on your infrastructure.
HTTP method: Select POST. Delivery events always use POST.
Request format: Select JSON.
Don't save yet. We'll configure event subscriptions and authentication in the next steps.
Step 2: Subscribe to delivery events
In the webhook creation form, find the Event subscriptions section. This is where you specify which events should trigger your webhook.
For message delivery tracking, subscribe to these three events:
conversation:message:delivery:channelconversation:message:delivery:userconversation:message:delivery:failure
You can subscribe to additional events if needed (like conversation:message for all messages), but for delivery tracking specifically, these three are what you want.
Event filtering options:
If your integration is part of the switchboard, you can control whether you receive events for conversations you're not actively managing. The deliverStandbyEvents setting filters this. Most integrations set this to false to only receive events for active conversations.
Save the webhook. Zendesk will assign it a unique ID (starting with 01). Copy this ID. You'll need it for testing and for connecting to triggers if you go that route.
Step 3: Set up webhook authentication
Webhooks without authentication are a security risk. Anyone who discovers your endpoint URL could send fake events. Zendesk supports several authentication methods.
Available authentication options
| Method | Best For | How It Works |
|---|---|---|
| API key | Simple integrations | Header-based key verification |
| Basic auth | Legacy system compatibility | Username/password in Authorization header |
| Bearer token | Modern API integrations | OAuth or JWT token verification |
For most implementations, API key authentication strikes the right balance between security and simplicity.
Configuring in Zendesk
In the webhook form, expand the Authentication section. Select your preferred method and enter the credentials:
- For API key: specify the header name (e.g.,
X-API-Key) and the key value - For Bearer token: enter the token Zendesk should include
- For Basic auth: provide username and password
Verifying webhook signatures
Zendesk can sign webhooks using HMAC-SHA256. This lets you verify that payloads actually came from Zendesk, not an attacker. See the Zendesk webhook signing documentation for detailed implementation guidance.
To enable this, you'll need to:
- Generate a signing secret (Zendesk provides this when you enable signing)
- Verify the
X-Zendesk-Webhook-Signatureheader in your endpoint - Compute the HMAC-SHA256 of the payload using your secret
- Compare it to the signature header
Here's a simple Node.js example:
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
Step 4: Build your webhook endpoint
Now for the fun part: building the endpoint that receives these webhooks.
Basic requirements
Your endpoint must:
- Use HTTPS (Zendesk rejects HTTP URLs)
- Respond within 10 seconds
- Return a 2xx status code for successful processing
- Handle duplicate events gracefully (idempotency)
Sample webhook receiver (Node.js/Express)
Here's a minimal but production-ready endpoint:
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/zendesk-delivery', (req, res) => {
// Acknowledge receipt immediately
res.status(200).send('OK');
// Process events asynchronously
const events = req.body.events || [];
for (const event of events) {
handleDeliveryEvent(event);
}
});
function handleDeliveryEvent(event) {
const { type, payload } = event;
switch (type) {
case 'conversation:message:delivery:channel':
console.log(`Message ${payload.message.id} delivered to ${payload.destination.type}`);
break;
case 'conversation:message:delivery:user':
console.log(`Message ${payload.message.id} reached user`);
break;
case 'conversation:message:delivery:failure':
console.error(`Message ${payload.message.id} failed:`, payload.error);
break;
}
}
app.listen(3000, () => {
console.log('Webhook endpoint listening on port 3000');
});
Key things this example does:
- Responds immediately (200 OK) before processing
- Processes events asynchronously
- Handles each event type separately
- Extracts relevant IDs for logging/debugging
Handling retries and idempotency
Zendesk retries failed webhooks (4xx/5xx responses, timeouts) up to 3-5 times depending on the error. This means your endpoint might receive the same event multiple times.
To handle this, implement idempotency:
const processedEvents = new Set(); // Use Redis in production
function handleDeliveryEvent(event) {
if (processedEvents.has(event.id)) {
console.log(`Skipping duplicate event: ${event.id}`);
return;
}
// Process the event...
processedEvents.add(event.id);
}
In production, use Redis or a database to track processed event IDs with TTL (time to live) so the deduplication window matches Zendesk's retry behavior.
Step 5: Understanding the delivery webhook payload
Let's break down what actually arrives in these webhooks.
Common payload structure
All delivery events share this wrapper structure:
{
"app": {
"id": "5fb29b20fee5422428712475"
},
"webhook": {
"id": "5ff5e98b0d0c6d8925594923",
"version": "v2"
},
"events": [
{
"id": "5ff7595eafcaab0a685ff889",
"createdAt": "2021-01-07T18:56:30.666Z",
"type": "conversation:message:delivery:channel",
"payload": {
"conversation": { "id": "2d4fd3d00715d1e64611e248" },
"user": { "id": "71268330a47f5c4b541fce46" },
"destination": {
"type": "whatsapp",
"integrationId": "5ff75853b1c3000a6ad4f7f5"
},
"message": { "id": "5ff7595eb1c3000a6ad4f7fb" },
"isFinalEvent": false,
"externalMessages": [
{ "id": "wamid.HBgNNTUxQTk4MDUz5Tg4MRU..." }
]
}
}
]
}
Key fields explained
| Field | Why It Matters |
|---|---|
events[].id | Unique event ID. Use for deduplication. |
events[].payload.conversation.id | Links the event to a specific conversation. |
events[].payload.message.id | The specific message this event refers to. |
events[].payload.destination.type | Which channel (whatsapp, twilio, etc.). |
events[].payload.isFinalEvent | Whether more events are coming for this message. |
events[].payload.externalMessages | Channel's native message IDs for debugging. |
Failure event specifics
When delivery fails, the payload includes an error object:
{
"error": {
"code": "bad_request",
"message": "Message failed to send because more than 24 hours have passed since the customer last replied.",
"underlyingError": {
"errors": [{
"code": 131047,
"title": "Re-engagement message",
"message": "Re-engagement message"
}]
}
}
}
The underlyingError field contains the raw error from the channel's API. This is invaluable for debugging. WhatsApp's error codes, for example, tell you exactly why a message was rejected.
Step 6: Test your webhook implementation
Before going live, you need to verify everything works.
Using Zendesk's webhook tester
In the webhook configuration page, click Test webhook. Zendesk will send a test payload to your endpoint and show you the response. This is useful for verifying connectivity and authentication.
However, the test payload is generic. It won't be a real delivery event. For realistic testing, you need to trigger actual message flows.
Testing with real messages
- Send a test message through Zendesk messaging
- Check your endpoint logs for incoming webhooks
- Verify the payload structure matches expectations
- Confirm your event handling logic processes each type correctly
For channels that support user delivery (like WhatsApp), you can verify the two-event flow. The first webhook should have isFinalEvent: false. The second (user delivery) should have isFinalEvent: true.
Checking webhook activity logs
Zendesk keeps webhook logs for 7 days. Navigate to Admin Center > Apps and integrations > Webhooks, select your webhook, and view the Activity tab. This shows recent deliveries, response codes, and any errors.
If you're not seeing expected webhooks, check:
- Is the webhook active (not paused)?
- Are the event subscriptions correct?
- Is your endpoint responding with 2xx status codes?
Troubleshooting common issues
Even with careful setup, things go wrong. Here are the most common issues and how to fix them.
Webhook not firing
If you're not receiving webhooks when messages are sent:
- Verify the webhook is active (not paused or in draft state)
- Check that the correct event types are subscribed
- For trigger-connected webhooks, verify the trigger conditions are being met
- Check the webhook activity logs for failure patterns
Authentication errors (401/403)
If Zendesk reports authentication failures:
- Verify your endpoint is checking the right header
- Confirm the API key/token hasn't expired
- Check for case sensitivity in header names
- If using signature verification, ensure you're computing the HMAC correctly
Timeouts and circuit breaker
Zendesk has a 10-second timeout. If your endpoint takes longer:
- Respond immediately (200 OK) and process asynchronously
- Use a message queue (SQS, RabbitMQ, etc.) for heavy processing
- Monitor processing times and optimize slow operations
The circuit breaker trips at 70% error rate or 1,000+ errors in 5 minutes. If this happens, Zendesk pauses your webhook. You'll need to fix the underlying issue and reactivate the webhook manually.
Missing delivery events
If you're seeing some events but not others:
- Check the channel support matrix. Not all channels send user delivery confirmations.
- For WhatsApp specifically, user delivery events may be delayed or suppressed if the message is received outside a certain time window.
- Verify
isFinalEventbehavior. If it'strueat the channel level, no user delivery event will follow.
Best practices for production
Once your webhook is working, consider these production recommendations.
Error handling and monitoring
- Log all webhook receipts with event IDs for debugging
- Set up alerts for elevated error rates
- Monitor endpoint response times
- Track delivery rates by channel to spot issues early
Security
- Always use HTTPS in production
- Implement webhook signature verification
- Rotate API keys periodically
- Don't log sensitive payload data
Data retention
Delivery events contain message metadata. Consider:
- How long to retain event logs
- Whether to store full payloads or just key fields
- Compliance requirements (GDPR, etc.) for message data
When to poll vs. use webhooks
Webhooks are ideal for real-time updates. But if you need historical data or missed events, you might need to supplement with polling. The List Messages API can fill gaps.
Streamline messaging workflows with eesel AI
Building custom webhook endpoints takes engineering time. You need to handle authentication, retries, idempotency, and error cases. Then you need to build the actual business logic on top of those events.
We built eesel AI to handle this complexity for you. Instead of managing webhooks yourself, you get turnkey messaging automation with delivery tracking built in. Our AI teammate integrates with Zendesk (and Freshdesk, Gorgias, and others) to handle conversations end-to-end.
If you're looking for a faster path to AI-powered messaging, check out our complete guide to Zendesk messaging webhooks. And if you want to compare options, see our breakdown of the best AI chatbots for Zendesk.
Frequently Asked Questions
Share this post

Article by
Stevia Putri
Stevia Putri is a marketing generalist at eesel AI, where she helps turn powerful AI tools into stories that resonate. She’s driven by curiosity, clarity, and the human side of technology.


