ChatKomodo Docs Home Sign in

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

Request
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

EventWhendata
message.receivedA message arrived on one of your devicesmessage_id, chat_id, direction (inbound), message_type, body
message.sentAn outgoing message was accepted by WhatsAppmessage_id, chat_id, direction, message_type, status
message.failedAn outgoing message could not be sentas above, plus error
session.connectedA device came onlinephone_number, wa_name
session.disconnectedA device went offlinereason: logged_out, banned or status_<code>
session.alertA device has been offline for more than 5 minutestype, message, disconnected_since, duration_ms
qr.generatedA new QR code is ready to scanqr, expires_at

Payload

Every delivery is a POST with a JSON body:

message.received
{
  "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:

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.

Node.js (Express)
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);
});
Python (Flask)
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
<?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

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.

Node.js
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.