Table of Contents
< All Topics

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

AspectPollingWebhooks
LatencyDepends on poll interval (seconds to minutes)Milliseconds (near-instant)
API CallsConstant, regardless of eventsOnly when events occur
Rate LimitsConsumes your quotaNo impact on quota
Server LoadHigher (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:

RequirementDetails
HTTPSRequired in production (HTTP allowed only in development)
Response TimeMust return HTTP 200 within 30 seconds
Public AccessMust be accessible from the internet (no localhost)
IdempotencyMust handle duplicate events gracefully

Step 2: Configure in Dashboard

  1. Navigate to External API Management
  2. Select your API key and click Edit
  3. Go to the Webhooks tab
  4. Enter your Webhook URL (e.g., https://api.yourcompany.com/webhooks/coext)
  5. Select which events to receive
  6. Click Save
  7. Click Test Webhook to verify your endpoint

Step 3: Event Selection

OptionEvents IncludedBest For
All Eventsqueued, sent, delivered, read, failed, receivedFull visibility into message lifecycle
Incoming Onlymessage.receivedChatbots, customer support systems
Delivery Onlysent, delivered, readDelivery tracking, analytics
Errors Onlymessage.failedAlerting, 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

TypeContent LocationExample
textmessage.text.bodyPlain text messages
imagemessage.image.idPhoto attachments
documentmessage.document.idPDF, Word files
audiomessage.audio.idVoice messages
videomessage.video.idVideo messages
locationmessage.locationShared locations
interactivemessage.interactiveButton/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:

HeaderDescriptionExample
X-Webhook-SignatureHMAC-SHA256 signature of the payloada1b2c3d4e5f6...
X-Webhook-TimestampUnix timestamp when webhook was sent1703001234
X-Webhook-EventEvent typemessage.received
User-AgentIdentifies Coext webhooksCoext-Webhook/1.0

Verifying Webhook Signatures

Always verify webhook signatures to ensure the request came from Coext.

Verification Algorithm

  1. Get the raw request body (before parsing as JSON)
  2. Extract X-Webhook-Signature and X-Webhook-Timestamp headers
  3. Recreate the payload: {timestamp}.{raw_body}
  4. Generate HMAC-SHA256 using your webhook secret
  5. 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:

AttemptDelay After FailureCumulative Time
1 (Initial)Immediate0 seconds
21 second1 second
35 seconds6 seconds
430 seconds36 seconds
55 minutes~5.5 minutes
6 (Final)30 minutes~35.5 minutes

Response Handling

Your ResponseWhat 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.

Leave a Reply

Your email address will not be published. Required fields are marked *