Skip to main content

Webhooks

Receive real-time notifications when message statuses change.

Overview

When you send a message via the API, it goes through several status updates:

sent → delivered → read

failed

Webhooks notify your server of these status changes so you can track delivery and take action.

Setup

1. Configure Webhook URL

  1. Go to Settings > Public API in your dashboard
  2. Enter your webhook endpoint URL
  3. Click Save

Your endpoint must:

  • Use HTTPS (required)
  • Accept POST requests
  • Return 200 status within 30 seconds

2. Get Your Webhook Secret

  1. Click Generate Secret (or Regenerate)
  2. Copy and store the secret securely
  3. Use it to verify webhook signatures

Webhook Payload

Headers

HeaderDescription
Content-Typeapplication/json
X-GBChat-SignatureHMAC-SHA256 signature
X-GBChat-TimestampUnix timestamp (seconds)

Body

{
"event": "message.delivered",
"timestamp": 1707300605000,
"data": {
"messageId": "msg_abc123xyz",
"externalId": "your-reference-123",
"status": "delivered",
"waMessageId": "wamid.HBgLMTIzNDU2Nzg5MA==",
"to": "919876543210",
"template": "order_confirmation",
"error": null
}
}

Event Types

EventDescription
message.sentMessage accepted by WhatsApp
message.deliveredMessage delivered to device
message.readMessage read by recipient
message.failedMessage delivery failed

Payload Fields

FieldTypeDescription
eventstringEvent type
timestampnumberEvent time (Unix ms)
data.messageIdstringGB Chat message ID
data.externalIdstringYour reference ID (if provided)
data.statusstringCurrent status
data.waMessageIdstringWhatsApp message ID
data.tostringRecipient phone
data.templatestringTemplate name
data.errorobject/nullError details (if failed)

Error Object (Failed Messages)

{
"event": "message.failed",
"timestamp": 1707300605000,
"data": {
"messageId": "msg_abc123xyz",
"status": "failed",
"error": {
"code": "131047",
"message": "Message failed to send because more than 24 hours have passed since the customer last replied"
}
}
}

Signature Verification

Always verify webhook signatures to ensure requests are from GB Chat.

How It Works

  1. We create a signature string: timestamp.payload
  2. We sign it with HMAC-SHA256 using your webhook secret
  3. We send the signature in the X-GBChat-Signature header

Verification Code

Node.js

const crypto = require('crypto');

function verifyWebhook(req, webhookSecret) {
const signature = req.headers['x-gbchat-signature'];
const timestamp = req.headers['x-gbchat-timestamp'];
const body = JSON.stringify(req.body);

// Check timestamp is recent (within 5 minutes)
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - parseInt(timestamp)) > 300) {
return false; // Replay attack protection
}

// Compute expected signature
const signaturePayload = `${timestamp}.${body}`;
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', webhookSecret)
.update(signaturePayload)
.digest('hex');

// Compare signatures (timing-safe)
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}

// Express middleware
app.post('/webhook', express.json(), (req, res) => {
if (!verifyWebhook(req, process.env.GBCHAT_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}

const { event, data } = req.body;
console.log(`Received ${event} for message ${data.messageId}`);

// Process the webhook...

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

Python

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

app = Flask(__name__)
WEBHOOK_SECRET = os.environ['GBCHAT_WEBHOOK_SECRET']

def verify_webhook(request):
signature = request.headers.get('X-GBChat-Signature', '')
timestamp = request.headers.get('X-GBChat-Timestamp', '')
body = request.get_data(as_text=True)

# Check timestamp is recent (within 5 minutes)
current_time = int(time.time())
if abs(current_time - int(timestamp)) > 300:
return False

# Compute expected signature
signature_payload = f"{timestamp}.{body}"
expected_signature = 'sha256=' + hmac.new(
WEBHOOK_SECRET.encode(),
signature_payload.encode(),
hashlib.sha256
).hexdigest()

# Compare signatures
return hmac.compare_digest(signature, expected_signature)

@app.route('/webhook', methods=['POST'])
def webhook():
if not verify_webhook(request):
return jsonify({'error': 'Invalid signature'}), 401

data = request.json
print(f"Received {data['event']} for message {data['data']['messageId']}")

# Process the webhook...

return 'OK', 200

Testing Webhooks

Test Button

  1. Go to Settings > Public API
  2. Make sure you have a webhook URL configured
  3. Click Test Webhook

This sends a test event to your endpoint:

{
"event": "webhook.test",
"timestamp": 1707300605000,
"data": {
"message": "This is a test webhook from GB Chat"
}
}

Local Development

For local testing, use a tunnel service:

# Using ngrok
ngrok http 3000

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

Best Practices

Respond Quickly

Return a 200 response immediately, then process asynchronously:

app.post('/webhook', (req, res) => {
// Respond immediately
res.status(200).send('OK');

// Process asynchronously
processWebhook(req.body).catch(console.error);
});

async function processWebhook(payload) {
// Update database, send notifications, etc.
}

Handle Duplicates

Webhooks may be delivered more than once. Use messageId for idempotency:

async function processWebhook(payload) {
const { messageId, status } = payload.data;

// Check if already processed
const existing = await db.webhooks.findOne({ messageId, status });
if (existing) {
console.log('Duplicate webhook, skipping');
return;
}

// Process and record
await db.webhooks.insert({ messageId, status, processedAt: new Date() });
await updateMessageStatus(messageId, status);
}

Retry Handling

If your endpoint returns an error, we retry:

AttemptDelay
1Immediate
21 minute
35 minutes

After 3 failed attempts, we stop retrying that webhook.

Log Everything

Keep logs for debugging:

app.post('/webhook', (req, res) => {
console.log('Webhook received:', {
event: req.body.event,
messageId: req.body.data.messageId,
timestamp: req.body.timestamp,
headers: {
signature: req.headers['x-gbchat-signature'],
timestamp: req.headers['x-gbchat-timestamp'],
},
});

// ...
});