. Create an API key under API keys.Guide
Webhooks and live events
ChatKomodo posts events to your HTTPS endpoint as they happen: incoming messages, delivery results and device changes.
Create a webhook
curl -X POST "https://api.chatkomodo.com/v1/webhooks" \
-H "Authorization: Bearer $CHATKOMODO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/chatkomodo",
"secret": "a-long-random-secret-of-32-characters-or-more",
"events": [
"message.received",
"message.sent",
"message.failed",
"session.connected",
"session.disconnected"
]
}'const res = await fetch('https://api.chatkomodo.com/v1/webhooks', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"url": "https://example.com/webhooks/chatkomodo",
"secret": "a-long-random-secret-of-32-characters-or-more",
"events": [
"message.received",
"message.sent",
"message.failed",
"session.connected",
"session.disconnected"
]
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.code}: ${data.error}`);
console.log(data);import os
import requests
res = requests.post(
"https://api.chatkomodo.com/v1/webhooks",
headers={
"Authorization": f"Bearer {os.environ['CHATKOMODO_API_KEY']}",
},
json={
"url": "https://example.com/webhooks/chatkomodo",
"secret": "a-long-random-secret-of-32-characters-or-more",
"events": [
"message.received",
"message.sent",
"message.failed",
"session.connected",
"session.disconnected",
],
},
timeout=30,
)
res.raise_for_status()
print(res.json())<?php
require 'vendor/autoload.php';
$client = new GuzzleHttp\Client();
$res = $client->request('POST', 'https://api.chatkomodo.com/v1/webhooks', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('CHATKOMODO_API_KEY'),
],
'json' => [
'url' => 'https://example.com/webhooks/chatkomodo',
'secret' => 'a-long-random-secret-of-32-characters-or-more',
'events' => [
'message.received',
'message.sent',
'message.failed',
'session.connected',
'session.disconnected',
],
],
]);
print_r(json_decode((string) $res->getBody(), true));The URL must be HTTPS and reachable from the internet; the secret at least 32 characters (keep it — it signs every delivery). Pause, change or delete it with PATCH /v1/webhooks/{id} and DELETE /v1/webhooks/{id}; send a webhook.test event with POST /v1/webhooks/{id}/test.
Events
| Event | When | data |
|---|---|---|
message.received | A message arrived on one of your devices | message_id, chat_id, direction (inbound), message_type, body |
message.sent | An outgoing message was accepted by WhatsApp | message_id, chat_id, direction, message_type, status |
message.failed | An outgoing message could not be sent | as above, plus error |
session.connected | A device came online | phone_number, wa_name |
session.disconnected | A device went offline | reason: logged_out, banned or status_<code> |
session.alert | A device has been offline for more than 5 minutes | type, message, disconnected_since, duration_ms |
qr.generated | A new QR code is ready to scan | qr, expires_at |
Payload
Every delivery is a POST with a JSON body:
{
"id": "evt_01JA5W9Q2R3S4T5V6W7X8Y9Z0A",
"type": "message.received",
"session_id": "01JA5V6Q8M2H7G4K9P3T0R1S2X",
"occurred_at": 1767225600000,
"data": {
"message_id": "01JA5W9Q2R3S4T5V6W7X8Y9Z0B",
"chat_id": "[email protected]",
"direction": "inbound",
"message_type": "text",
"body": "Is the red one still in stock?"
}
}and these headers:
X-Webhook-Signature:sha256=followed by the base64 HMAC-SHA256 of the raw body, with your webhook secret as the keyX-Webhook-Event: the event typeX-Webhook-ID: the event id (the same on every retry)
Verify the signature
Compute the HMAC over the raw request body (before any JSON parsing) and compare in constant time. Reject anything that does not match.
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
const app = express();
const secret = process.env.CHATKOMODO_WEBHOOK_SECRET;
app.post('/webhooks/chatkomodo', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + createHmac('sha256', secret).update(req.body).digest('base64');
const got = req.get('X-Webhook-Signature') ?? '';
if (got.length !== expected.length || !timingSafeEqual(Buffer.from(got), Buffer.from(expected))) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString('utf8'));
res.status(200).end(); // answer first, then work
handle(event);
});import base64, hashlib, hmac, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["CHATKOMODO_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/chatkomodo")
def chatkomodo():
raw = request.get_data()
expected = "sha256=" + base64.b64encode(hmac.new(SECRET, raw, hashlib.sha256).digest()).decode()
if not hmac.compare_digest(expected, request.headers.get("X-Webhook-Signature", "")):
abort(401)
event = request.get_json()
# queue the work, answer quickly
return "", 200<?php
$raw = file_get_contents('php://input');
$expected = 'sha256=' . base64_encode(hash_hmac('sha256', $raw, getenv('CHATKOMODO_WEBHOOK_SECRET'), true));
if (!hash_equals($expected, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
http_response_code(200);Answer fast; retries
- Answer with any
2xxwithin 10 seconds. Do slow work after answering (a job queue). - Anything else — another status, a timeout, a connection error — is retried up to 4 times: after 30 seconds, 2 minutes, 10 minutes and 1 hour.
- The same event can arrive twice and events can arrive out of order: deduplicate by
X-Webhook-IDand useoccurred_at. GET /v1/webhooks/deliveriesshows every attempt with the status your server answered — the first place to look when events seem missing.- Addresses on private or local networks are refused (the URL must resolve to a public address).
Live event stream (SSE)
For a live screen, open a Server-Sent Events stream instead: get a one-minute ticket with POST /v1/events-ticket, then connect to the url it returns.
import { EventSource } from 'eventsource'; // npm i eventsource
const API = 'https://api.chatkomodo.com';
const res = await fetch(`${API}/v1/events-ticket`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.CHATKOMODO_API_KEY}` },
});
const { url } = await res.json(); // "/v1/events?ticket=…", valid for 60 seconds
const events = new EventSource(API + url);
events.addEventListener('message.received', (e) => console.log(JSON.parse(e.data)));
events.addEventListener('blast.progress', (e) => console.log(JSON.parse(e.data)));The stream carries the webhook events plus blast.progress, blast.waiting, blast.paused, blast.completed, scheduled.sent and recurring.executed. Events sent while you were disconnected are not replayed: use webhooks when every event matters.
Incoming store webhooks
Shopify and WooCommerce can call ChatKomodo too, to send order messages to buyers: see Online stores.