Webhook Integration – Receiving Real-Time Notifications
What Are Webhooks?
Webhooks are HTTP callbacks that notify your application about events in real-time. Instead of constantly polling the API to check for updates, your server receives an instant notification the moment something happens.
Webhooks vs Polling
| Aspect | Polling | Webhooks |
|---|---|---|
| Latency | Depends on poll interval (seconds to minutes) | Milliseconds (near-instant) |
| API Calls | Constant, regardless of events | Only when events occur |
| Rate Limits | Consumes your quota | No impact on quota |
| Server Load | Higher (constant requests) | Lower (event-driven) |
Webhook Use Cases
- Incoming Customer Messages: Get notified instantly when a customer sends you a message, enabling real-time conversations
- Delivery Confirmations: Know exactly when your messages are delivered and read
- Error Handling: Receive immediate alerts when messages fail, allowing quick remediation
- Analytics: Track message lifecycle for reporting and optimization
- Integration Triggers: Automatically trigger workflows in your CRM, ticketing system, or automation platform
Setting Up Webhooks
Step 1: Prepare Your Endpoint
Your webhook endpoint must:
| Requirement | Details |
|---|---|
| HTTPS | Required in production (HTTP allowed only in development) |
| Response Time | Must return HTTP 200 within 30 seconds |
| Public Access | Must be accessible from the internet (no localhost) |
| Idempotency | Must handle duplicate events gracefully |
Step 2: Configure in Dashboard
- Navigate to External API Management
- Select your API key and click Edit
- Go to the Webhooks tab
- Enter your Webhook URL (e.g.,
https://api.yourcompany.com/webhooks/coext) - Select which events to receive
- Click Save
- Click Test Webhook to verify your endpoint
Step 3: Event Selection
| Option | Events Included | Best For |
|---|---|---|
| All Events | queued, sent, delivered, read, failed, received | Full visibility into message lifecycle |
| Incoming Only | message.received | Chatbots, customer support systems |
| Delivery Only | sent, delivered, read | Delivery tracking, analytics |
| Errors Only | message.failed | Alerting, error monitoring |
Event Types and Payloads
Incoming Message (message.received)
Triggered when a customer sends a message to your business number.
{
"event": "message.received",
"timestamp": "2025-12-19T10:00:00.000Z",
"data": {
"message_id": "wamid.HBgMOTE5ODc2NTQzMjEwFQIAEhgUM0EB...",
"from": "+919876543210",
"to": "+14155238886",
"message": {
"type": "text",
"text": {
"body": "Hello, I have a question about my order"
}
},
"contact": {
"name": "John Doe",
"wa_id": "919876543210"
}
}
}
Message Types
| Type | Content Location | Example |
|---|---|---|
text | message.text.body | Plain text messages |
image | message.image.id | Photo attachments |
document | message.document.id | PDF, Word files |
audio | message.audio.id | Voice messages |
video | message.video.id | Video messages |
location | message.location | Shared locations |
interactive | message.interactive | Button/list replies |
Delivery Status (message.delivered)
Triggered when your outgoing message is delivered to the recipient’s device.
{
"event": "message.delivered",
"timestamp": "2025-12-19T10:00:05.000Z",
"data": {
"message_id": "550e8400-e29b-41d4-a716-446655440000",
"reference": "order-12345",
"status": "delivered",
"recipient": "+919876543210",
"delivered_at": "2025-12-19T10:00:05.000Z"
}
}
Message Read (message.read)
Triggered when the recipient opens and reads your message (only if read receipts are enabled).
{
"event": "message.read",
"timestamp": "2025-12-19T10:01:30.000Z",
"data": {
"message_id": "550e8400-e29b-41d4-a716-446655440000",
"reference": "order-12345",
"status": "read",
"recipient": "+919876543210",
"read_at": "2025-12-19T10:01:30.000Z"
}
}
Message Failed (message.failed)
Triggered when message delivery fails.
{
"event": "message.failed",
"timestamp": "2025-12-19T10:00:10.000Z",
"data": {
"message_id": "550e8400-e29b-41d4-a716-446655440000",
"reference": "order-12345",
"status": "failed",
"recipient": "+919876543210",
"error": {
"code": "131047",
"message": "Re-engagement message required - use a template"
}
}
}
Test Webhook (webhook.test)
Sent when you click “Test Webhook” in the dashboard.
{
"event": "webhook.test",
"timestamp": "2025-12-19T10:00:00.000Z",
"data": {
"test": true,
"message": "This is a test webhook to verify your endpoint"
}
}
Webhook Security Headers
Every webhook includes these security headers for verification:
| Header | Description | Example |
|---|---|---|
X-Webhook-Signature | HMAC-SHA256 signature of the payload | a1b2c3d4e5f6... |
X-Webhook-Timestamp | Unix timestamp when webhook was sent | 1703001234 |
X-Webhook-Event | Event type | message.received |
User-Agent | Identifies Coext webhooks | Coext-Webhook/1.0 |
Verifying Webhook Signatures
Always verify webhook signatures to ensure the request came from Coext.
Verification Algorithm
- Get the raw request body (before parsing as JSON)
- Extract
X-Webhook-SignatureandX-Webhook-Timestampheaders - Recreate the payload:
{timestamp}.{raw_body} - Generate HMAC-SHA256 using your webhook secret
- Compare signatures using constant-time comparison
Node.js/Express Implementation
const crypto = require('crypto');
const express = require('express');
const WEBHOOK_SECRET = process.env.COEXT_WEBHOOK_SECRET;
function verifyWebhookSignature(rawBody, signature, timestamp) {
// Recreate the signature payload
const payload = `${timestamp}.${rawBody}`;
// Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
try {
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
} catch {
return false;
}
}
const app = express();
// Use raw body parser for webhook routes
app.post('/webhooks/coext',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const rawBody = req.body.toString();
// Verify signature
if (!verifyWebhookSignature(rawBody, signature, timestamp)) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}
// Parse JSON and process
const payload = JSON.parse(rawBody);
// Return 200 immediately (critical!)
res.status(200).json({ received: true });
// Process webhook asynchronously
processWebhook(payload).catch(console.error);
}
);
async function processWebhook(payload) {
const { event, data } = payload;
switch (event) {
case 'message.received':
console.log(`New message from ${data.from}: ${data.message.text?.body}`);
// Handle incoming message
break;
case 'message.delivered':
console.log(`Message ${data.message_id} delivered`);
// Update delivery status
break;
case 'message.failed':
console.error(`Message ${data.message_id} failed: ${data.error?.message}`);
// Handle failure, maybe retry
break;
}
}
Python/Flask Implementation
import hashlib
import hmac
import os
from flask import Flask, request, jsonify
WEBHOOK_SECRET = os.environ['COEXT_WEBHOOK_SECRET']
def verify_webhook_signature(raw_body: bytes, signature: str, timestamp: str) -> bool:
payload = f"{timestamp}.{raw_body.decode()}"
expected = hmac.new(
WEBHOOK_SECRET.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
app = Flask(__name__)
@app.route('/webhooks/coext', methods=['POST'])
def handle_webhook():
raw_body = request.get_data()
signature = request.headers.get('X-Webhook-Signature', '')
timestamp = request.headers.get('X-Webhook-Timestamp', '')
if not verify_webhook_signature(raw_body, signature, timestamp):
return 'Invalid signature', 401
payload = request.json
event = payload.get('event')
data = payload.get('data', {})
# Log for debugging
print(f"Received webhook: {event}")
if event == 'message.received':
text = data.get('message', {}).get('text', {}).get('body', '')
print(f"New message from {data.get('from')}: {text}")
elif event == 'message.failed':
print(f"Message failed: {data.get('error', {}).get('message')}")
return jsonify({'received': True}), 200
Retry Behavior
If your endpoint doesn’t respond with HTTP 200, Coext automatically retries with exponential backoff:
| Attempt | Delay After Failure | Cumulative Time |
|---|---|---|
| 1 (Initial) | Immediate | 0 seconds |
| 2 | 1 second | 1 second |
| 3 | 5 seconds | 6 seconds |
| 4 | 30 seconds | 36 seconds |
| 5 | 5 minutes | ~5.5 minutes |
| 6 (Final) | 30 minutes | ~35.5 minutes |
Response Handling
| Your Response | What Happens |
|---|---|
| HTTP 200-299 | ✓ Success – no retry |
| HTTP 4xx (except 410) | ↻ Retry scheduled |
| HTTP 410 Gone | ✗ Permanent failure – no retry, webhook disabled |
| HTTP 5xx | ↻ Retry scheduled |
| Timeout (>30s) | ↻ Retry scheduled |
Best Practices
1. Return 200 Immediately
Always acknowledge the webhook before processing. Heavy processing should happen asynchronously.
// ✅ Good: Return quickly, process async
app.post('/webhook', (req, res) => {
res.status(200).send('OK'); // Return immediately
processWebhook(req.body); // Process in background
});
// ❌ Bad: Process before responding
app.post('/webhook', async (req, res) => {
await heavyProcessing(req.body); // May timeout!
res.status(200).send('OK');
});
2. Handle Duplicates (Idempotency)
Webhooks may be delivered more than once. Use message_id to deduplicate.
async function processWebhook(payload) {
const messageId = payload.data.message_id;
// Check if already processed
const existing = await db.webhooks.findOne({ messageId });
if (existing) {
console.log('Duplicate webhook, skipping');
return;
}
// Process and record
await handleEvent(payload);
await db.webhooks.insertOne({
messageId,
processedAt: new Date()
});
}
3. Verify All Signatures
Never trust webhooks without signature verification in production.
4. Log Everything
Comprehensive logging helps with debugging and auditing.
console.log('Webhook received:', {
event: payload.event,
timestamp: payload.timestamp,
messageId: payload.data?.message_id,
receivedAt: new Date().toISOString()
});
5. Use a Message Queue for Heavy Processing
For complex workflows, push webhooks to a queue (e.g., Redis, RabbitMQ) and process separately.

