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
- Go to Settings > Public API in your dashboard
- Enter your webhook endpoint URL
- Click Save
Your endpoint must:
- Use HTTPS (required)
- Accept POST requests
- Return 200 status within 30 seconds
2. Get Your Webhook Secret
- Click Generate Secret (or Regenerate)
- Copy and store the secret securely
- Use it to verify webhook signatures
Webhook Payload
Headers
| Header | Description |
|---|---|
Content-Type | application/json |
X-GBChat-Signature | HMAC-SHA256 signature |
X-GBChat-Timestamp | Unix 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
| Event | Description |
|---|---|
message.sent | Message accepted by WhatsApp |
message.delivered | Message delivered to device |
message.read | Message read by recipient |
message.failed | Message delivery failed |
Payload Fields
| Field | Type | Description |
|---|---|---|
event | string | Event type |
timestamp | number | Event time (Unix ms) |
data.messageId | string | GB Chat message ID |
data.externalId | string | Your reference ID (if provided) |
data.status | string | Current status |
data.waMessageId | string | WhatsApp message ID |
data.to | string | Recipient phone |
data.template | string | Template name |
data.error | object/null | Error 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
- We create a signature string:
timestamp.payload - We sign it with HMAC-SHA256 using your webhook secret
- We send the signature in the
X-GBChat-Signatureheader
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
- Go to Settings > Public API
- Make sure you have a webhook URL configured
- 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:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 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'],
},
});
// ...
});