Real-Time Events
Instant Delivery
Receive real-time notifications for incoming messages, delivery status updates, and message failures. Build reactive applications with instant event delivery.
Instant Delivery
Verified Signature
3 Attempts + Backoff
Webhooks deliver real-time notifications directly to your application when events occur. Instead of polling our API every few seconds, we send instant HTTP POST requests to your server when messages arrive, are delivered, or fail. This eliminates latency and reduces unnecessary API calls.
Customer sends you a message or message delivery status changes on WhatsApp.
Chat Mitra queues the event for delivery with automatic retry logic to ensure it reaches your server.
We POST the event payload to your registered webhook URL with HMAC signature for authentication.
Your server returns 200 OK. If not, we automatically retry with exponential backoff (up to 3 times total).
message.received - Customer sends you a message | message.sent - Your message delivered to WhatsApp | message.failed - Message delivery failed | message.status.updated - Status change (delivered/read)
Create an HTTP endpoint on your server that will receive webhook events from Chat Mitra. Your endpoint must be publicly accessible via HTTPS.
// Node.js / Express
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json());
// Store webhook secret securely from environment
const WEBHOOK_SECRET = process.env.CHATMITRA_WEBHOOK_SECRET;
app.post('/webhooks/chatmitra', (req, res) => {
// 1. Get signature from header
const signature = req.headers['x-webhook-signature'];
// 2. Get raw body and verify signature
const body = JSON.stringify(req.body);
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(body)
.digest('hex');
if (signature !== expectedSignature) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 3. Process event
const event = req.body;
console.log('Received event:', event.event);
// Handle different event types
switch (event.event) {
case 'message.received':
handleIncomingMessage(event.message);
break;
case 'message.sent':
handleMessageSent(event.message);
break;
case 'message.failed':
handleMessageFailed(event.message);
break;
case 'message.status.updated':
handleStatusUpdate(event.status);
break;
}
// 4. Return 200 OK
return res.json({ ok: true });
});
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});# Python / Flask
import os
import hmac
import hashlib
import json
from flask import Flask, request
app = Flask(__name__)
WEBHOOK_SECRET = os.getenv('CHATMITRA_WEBHOOK_SECRET')
@app.route('/webhooks/chatmitra', methods=['POST'])
def webhook():
# 1. Get signature from header
signature = request.headers.get('X-Webhook-Signature')
# 2. Get raw body and verify signature
body = request.get_data(as_text=True)
expected_sig = hmac.new(
WEBHOOK_SECRET.encode(),
body.encode(),
hashlib.sha256
).hexdigest()
if signature != expected_sig:
return {'error': 'Invalid signature'}, 401
# 3. Process event
event = json.loads(body)
print(f'Received event: {event.get("event")}')
# Handle different event types
event_type = event.get('event')
if event_type == 'message.received':
handle_incoming_message(event.get('message'))
elif event_type == 'message.sent':
handle_message_sent(event.get('message'))
elif event_type == 'message.failed':
handle_message_failed(event.get('message'))
elif event_type == 'message.status.updated':
handle_status_update(event.get('status'))
# 4. Return 200 OK
return {'ok': True}, 200
if __name__ == '__main__':
app.run(port=3000)<?php
// PHP / Laravel
$webhookSecret = getenv('CHATMITRA_WEBHOOK_SECRET');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? null;
// Get raw body
$body = file_get_contents('php://input');
// Verify signature
$expectedSignature = hash_hmac('sha256', $body, $webhookSecret);
if ($signature !== $expectedSignature) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Decode and process event
$event = json_decode($body, true);
echo "Received event: " . $event['event'] . "\n";
// Handle different event types
switch ($event['event']) {
case 'message.received':
handleIncomingMessage($event['message']);
break;
case 'message.sent':
handleMessageSent($event['message']);
break;
case 'message.failed':
handleMessageFailed($event['message']);
break;
case 'message.status.updated':
handleStatusUpdate($event['status']);
break;
}
// Return 200 OK
http_response_code(200);
echo json_encode(['ok' => true]);// Java / Spring Boot
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Hex;
@RestController
@RequestMapping("/webhooks")
public class WebhookController {
private final String WEBHOOK_SECRET = System.getenv("CHATMITRA_WEBHOOK_SECRET");
@PostMapping("/chatmitra")
public ResponseEntity<?> handleWebhook(
@RequestBody String body,
@RequestHeader("X-Webhook-Signature") String signature) {
try {
// Verify signature
String expectedSignature = generateSignature(body);
if (!signature.equals(expectedSignature)) {
return ResponseEntity.status(401).body(
Map.of("error", "Invalid signature")
);
}
// Parse and process event
ObjectMapper mapper = new ObjectMapper();
JsonNode event = mapper.readTree(body);
String eventType = event.get("event").asText();
System.out.println("Received event: " + eventType);
// Handle different event types
switch (eventType) {
case "message.received":
handleIncomingMessage(event.get("message"));
break;
case "message.sent":
handleMessageSent(event.get("message"));
break;
case "message.failed":
handleMessageFailed(event.get("message"));
break;
case "message.status.updated":
handleStatusUpdate(event.get("status"));
break;
}
return ResponseEntity.ok(Map.of("ok", true));
} catch (Exception e) {
return ResponseEntity.status(400).body(
Map.of("error", e.getMessage())
);
}
}
private String generateSignature(String body) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec key = new SecretKeySpec(
WEBHOOK_SECRET.getBytes(),
"HmacSHA256"
);
mac.init(key);
return Hex.encodeHexString(mac.doFinal(body.getBytes()));
}
}// Go / Gin
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.POST("/webhooks/chatmitra", handleWebhook)
log.Fatal(router.Run(":3000"))
}
func handleWebhook(c *gin.Context) {
webhookSecret := os.Getenv("CHATMITRA_WEBHOOK_SECRET")
signature := c.GetHeader("X-Webhook-Signature")
// Read raw body
body, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(400, gin.H{"error": "Failed to read body"})
return
}
// Verify signature
expectedSignature := generateSignature(string(body), webhookSecret)
if signature != expectedSignature {
c.JSON(401, gin.H{"error": "Invalid signature"})
return
}
// Parse and process event
var event map[string]interface{}
if err := json.Unmarshal(body, &event); err != nil {
c.JSON(400, gin.H{"error": "Invalid JSON"})
return
}
eventType := event["event"].(string)
fmt.Printf("Received event: %s\n", eventType)
// Handle different event types
switch eventType {
case "message.received":
handleIncomingMessage(event["message"])
case "message.sent":
handleMessageSent(event["message"])
case "message.failed":
handleMessageFailed(event["message"])
case "message.status.updated":
handleStatusUpdate(event["status"])
}
c.JSON(200, gin.H{"ok": true})
}
func generateSignature(body string, secret string) string {
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(body))
return hex.EncodeToString(h.Sum(nil))
}Store your webhook secret securely in environment variables:
# .env
CHATMITRA_WEBHOOK_SECRET=whsec_abc123xyz789_secure_key
WEBHOOK_PORT=3000
NODE_ENV=productionNever commit your webhook secret to version control. Always use environment variables and rotate secrets periodically. Treat this secret like a password.
Your webhook endpoint must meet these requirements:
Use ngrok, LocalTunnel, or Cloudflare Tunnel to expose your local server to the internet during development. Example: ngrok http 3000 provides a public HTTPS URL for testing.
Visit https://app.chatmitra.com/signin and log in with your registered email and password.
Go to Dashboard → Settings → API & Integrations → Webhooks to manage your webhook configurations.
Click the Add Webhook button and enter your public HTTPS URL where you want to receive events.
Choose which events to subscribe to: message.received, message.sent, message.failed, and/or message.status.updated.
Click Create Webhook. Copy your webhook secret immediately and store it in your .env file. You cannot retrieve it later.
Use the Test Webhook button in the dashboard to send a sample event. Verify your endpoint returns 200 OK.
Once your webhook is registered and tested successfully, you'll start receiving real-time events immediately.
Chat Mitra sends webhooks for four event types. Each event has a specific payload structure that you can use to process different types of notifications.
Triggered when a customer sends you a message on WhatsApp. Use this to log incoming messages, send auto-replies, or route to your support system.
{
"event": "message.received",
"message_id": "wamid_abc123",
"direction": "inbound",
"from": "919999999999",
"to": "919888888888",
"timestamp": 1705329000,
"message": {
"type": "text",
"text": "Hi, I have a question about my order"
}
}The message object contains different fields based on type:
text
{ "type": "text", "text": "Hello" }image
{ "type": "image", "media": { "url": "https://...", "mime_type": "image/jpeg" } }video
{ "type": "video", "media": { "url": "https://...", "mime_type": "video/mp4" } }document
{ "type": "document", "media": { "url": "https://...", "filename": "invoice.pdf" } }location
{ "type": "location", "location": { "latitude": 23.0225, "longitude": 72.5714 } }interactive
{ "type": "interactive", "interactive_type": "button_reply", "body": "Select option" }Triggered when your message is successfully sent via WhatsApp API. Use this to log message delivery or update message status in your database.
{
"event": "message.sent",
"message_id": "wamid_def456",
"direction": "outbound",
"from": "919888888888",
"to": "919999999999",
"timestamp": 1705329030,
"message": {
"type": "text",
"text": "Thank you for contacting us!"
}
}Triggered when a message fails to deliver. Check the fail_reason to understand why and take appropriate action.
{
"event": "message.failed",
"message_id": null,
"direction": "outbound",
"from": "919888888888",
"to": "919999999999",
"timestamp": 1705329060,
"status": "failed",
"fail_reason": "invalid_phone_number",
"message": {
"type": "text",
"text": "Your package is ready"
}
}invalid_phone_number - Phone number has invalid format or doesn't existinsufficient_credits - Your account doesn't have enough creditstemplate_not_approved - Template is pending approval or was rejectedrate_limited - Too many requests in a short time periodTriggered when message status changes (sent → delivered → read). Use this to track message delivery and engagement metrics.
{
"event": "message.status.updated",
"message_id": "wamid_def456",
"timestamp": 1705329045,
"status": "delivered"
}
// Or when customer reads the message:
{
"event": "message.status.updated",
"message_id": "wamid_def456",
"timestamp": 1705329075,
"status": "read"
}sent - Message successfully sent to WhatsApp serversdelivered - Message reached customer's deviceread - Customer opened and read the messagefailed - Message delivery failed permanentlyEvery webhook request is signed with HMAC-SHA256. Always verify the signature before processing events to ensure requests genuinely come from Chat Mitra and haven't been tampered with.
The X-Webhook-Signature header contains the HMAC-SHA256 hash of the request body. Calculate the hash using your webhook secret and compare it to verify authenticity.
import crypto from 'crypto';
import express from 'express';
const app = express();
app.use(express.raw({ type: 'application/json' }));
// Function to verify webhook signature
function verifyWebhookSignature(body, signature, secret) {
// body must be the raw request body as a string
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
// Use constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
const WEBHOOK_SECRET = process.env.CHATMITRA_WEBHOOK_SECRET;
app.post('/webhooks/chatmitra', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const body = req.body.toString('utf8');
try {
// Verify signature
if (!verifyWebhookSignature(body, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Parse and process event
const event = JSON.parse(body);
console.log('Processing event:', event.event);
return res.json({ ok: true });
} catch (err) {
console.error('Webhook error:', err);
return res.status(400).json({ error: err.message });
}
});
app.listen(3000);import hmac
import hashlib
import json
import os
from flask import Flask, request
app = Flask(__name__)
WEBHOOK_SECRET = os.getenv('CHATMITRA_WEBHOOK_SECRET')
def verify_webhook_signature(body, signature, secret):
"""Verify HMAC-SHA256 signature using constant-time comparison"""
expected_sig = hmac.new(
secret.encode(),
body.encode(),
hashlib.sha256
).hexdigest()
# Use constant-time comparison to prevent timing attacks
return hmac.compare_digest(signature, expected_sig)
@app.route('/webhooks/chatmitra', methods=['POST'])
def webhook():
signature = request.headers.get('X-Webhook-Signature')
body = request.get_data(as_text=True)
try:
# Verify signature
if not verify_webhook_signature(body, signature, WEBHOOK_SECRET):
return {'error': 'Invalid signature'}, 401
# Parse and process event
event = json.loads(body)
print(f'Processing event: {event.get("event")}')
return {'ok': True}, 200
except Exception as e:
print(f'Webhook error: {e}')
return {'error': str(e)}, 400
if __name__ == '__main__':
app.run(port=3000)<?php
$webhookSecret = getenv('CHATMITRA_WEBHOOK_SECRET');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? null;
// Get raw body
$body = file_get_contents('php://input');
// Verify signature using hash_hmac
$expectedSignature = hash_hmac('sha256', $body, $webhookSecret);
// Use hash_equals for constant-time comparison
if (!hash_equals($signature, $expectedSignature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
try {
// Parse and process event
$event = json_decode($body, true);
if (!$event) {
throw new Exception('Invalid JSON');
}
echo "Processing event: " . $event['event'] . "\n";
http_response_code(200);
echo json_encode(['ok' => true]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
}import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;
@RestController
@RequestMapping("/webhooks")
public class WebhookController {
private final String WEBHOOK_SECRET = System.getenv("CHATMITRA_WEBHOOK_SECRET");
private String generateSignature(String body) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(
WEBHOOK_SECRET.getBytes(),
0,
WEBHOOK_SECRET.getBytes().length,
"HmacSHA256"
);
mac.init(keySpec);
byte[] rawHmac = mac.doFinal(body.getBytes());
// Convert bytes to hex string
StringBuilder hexString = new StringBuilder();
for (byte b : rawHmac) {
hexString.append(String.format("%02x", b));
}
return hexString.toString();
}
private boolean constantTimeEquals(String a, String b) {
if (a.length() != b.length()) {
return false;
}
int result = 0;
for (int i = 0; i < a.length(); i++) {
result |= a.charAt(i) ^ b.charAt(i);
}
return result == 0;
}
@PostMapping("/chatmitra")
public ResponseEntity<?> handleWebhook(
@RequestBody String body,
@RequestHeader("X-Webhook-Signature") String signature) {
try {
// Verify signature
String expectedSignature = generateSignature(body);
if (!constantTimeEquals(signature, expectedSignature)) {
return ResponseEntity.status(401)
.body(Map.of("error", "Invalid signature"));
}
// Parse and process event
System.out.println("Processing webhook event");
return ResponseEntity.ok(Map.of("ok", true));
} catch (Exception e) {
return ResponseEntity.status(400)
.body(Map.of("error", e.getMessage()));
}
}
}package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"github.com/gin-gonic/gin"
)
func generateSignature(body string, secret string) string {
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(body))
return hex.EncodeToString(h.Sum(nil))
}
func constantTimeEquals(a, b string) bool {
if len(a) != len(b) {
return false
}
result := 0
for i := 0; i < len(a); i++ {
result |= int(a[i]) ^ int(b[i])
}
return result == 0
}
func handleWebhook(c *gin.Context) {
webhookSecret := os.Getenv("CHATMITRA_WEBHOOK_SECRET")
signature := c.GetHeader("X-Webhook-Signature")
// Read raw body
body, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Failed to read body",
})
return
}
bodyStr := string(body)
// Verify signature using constant-time comparison
expectedSignature := generateSignature(bodyStr, webhookSecret)
if !constantTimeEquals(signature, expectedSignature) {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Invalid signature",
})
return
}
// Parse and process event
var event map[string]interface{}
if err := json.Unmarshal(body, &event); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Invalid JSON",
})
return
}
eventType := event["event"].(string)
fmt.Printf("Processing event: %s\n", eventType)
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func main() {
router := gin.Default()
router.POST("/webhooks/chatmitra", handleWebhook)
log.Fatal(router.Run(":3000"))
}Never process webhook requests without verifying the HMAC-SHA256 signature. This ensures the request came from Chat Mitra and protects against replay attacks or man-in-the-middle threats.
Always register HTTPS URLs (not HTTP). Chat Mitra will only send webhooks to encrypted endpoints for data protection.
Always check the HMAC-SHA256 signature on every incoming webhook before processing any data or triggering actions.
Use constant-time string comparison (e.g., crypto.timingSafeEqual in Node.js) to prevent timing attacks on signature verification.
We retry failed webhooks up to 3 times. Use message_id to detect and skip duplicate events from retries.
Return 200 OK immediately. Queue long-running tasks asynchronously and respond within 30 seconds to prevent timeouts.
Log incoming webhooks with timestamp and message_id for debugging and audit trails. Never log the webhook secret.
Store secrets in environment variables only. Never commit to version control. Rotate secrets periodically and after any suspected compromise.
If your endpoint returns a non-2xx status code or times out, Chat Mitra automatically retries with exponential backoff:
Since we retry failed webhooks, you may receive the same event multiple times. Use the message_id field to track processed events and skip duplicates to ensure idempotent processing.
Set up webhooks in minutes and build real-time applications with instant event notifications from Chat Mitra.
Go to Dashboard