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'],
},
});

// ...
});