ChatKazi
API reference

Webhooks

Receive inbound WhatsApp messages and verify webhook signatures.

Webhooks let your backend receive inbound WhatsApp messages as they arrive.

ChatKazi sends each event as an HTTP POST request to your configured URL. Your endpoint should verify the request signature before processing the event and return a 2xx response within 10 seconds.

How webhooks work

Configure an endpoint

Save a public HTTP or HTTPS URL where ChatKazi can send events.

Store the signing secret

The first save returns a signing secret beginning with whsec_. Store it securely because later reads only show whether a secret exists.

Verify every request

Use the signing secret, timestamp header, and raw request body to verify the X-ChatKazi-Signature header.

Process the event

After verification, parse the JSON body and handle the event. Return any 2xx status to mark the delivery as successful.

Configure a webhook

PATCH /webhook
Terminal
curl -X PATCH "$CHATKAZI_BASE_URL/webhook" \
  -H "x-api-key: $CHATKAZI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/chatkazi",
    "enabled": true
  }'

The first save returns the signing secret once:

Response
{
  "message": "Webhook saved.",
  "data": {
    "url": "https://your-app.com/webhooks/chatkazi",
    "enabled": true,
    "hasSecret": true
  },
  "secret": "whsec_your_signing_secret"
}
Store the signing secret in your backend environment variables or secret manager. Do not expose it in browser code, logs, or source control.

Webhook request headers

Every delivery includes these headers:

HeaderDescription
X-ChatKazi-EventEvent type, such as message.received.
X-ChatKazi-DeliveryUnique delivery ID for logging and deduplication.
X-ChatKazi-TimestampISO 8601 time used to create the signature.
X-ChatKazi-SignatureHMAC-SHA256 signature in the form sha256=<hex_digest>.

ChatKazi creates the signature from:

timestamp + "." + raw_request_body

The request body must be used exactly as received. Re-serializing parsed JSON can produce a different signature.

Signing secret scope

Each ChatKazi account has one configured webhook destination and one signing secret. The same secret signs events from every WhatsApp session owned by that account.

  • A valid signature proves that ChatKazi created the request for your account.
  • The signature does not identify a specific WhatsApp session. Use the payload's sessionId for that.
  • Rotating the signing secret immediately replaces the previous secret.
  • If separate systems need to receive events, configure one webhook endpoint and forward verified events from that endpoint.

Verify the signing secret

Set your signing secret on the server:

.env
CHATKAZI_WEBHOOK_SECRET=whsec_your_signing_secret

In Express, register the webhook route with express.raw() before any global express.json() middleware:

webhook-server.js
import crypto from 'node:crypto'
import express from 'express'

const app = express()

app.post('/webhooks/chatkazi', express.raw({ type: 'application/json' }), (req, res) => {
  const secret = process.env.CHATKAZI_WEBHOOK_SECRET
  const timestamp = req.get('X-ChatKazi-Timestamp')
  const receivedSignature = req.get('X-ChatKazi-Signature')
  const rawBody = req.body.toString('utf8')

  if (!secret || !timestamp || !receivedSignature) {
    return res.status(400).send('Missing webhook signature')
  }

  const expectedSignature = `sha256=${crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')}`

  const received = Buffer.from(receivedSignature)
  const expected = Buffer.from(expectedSignature)
  const valid =
    received.length === expected.length &&
    crypto.timingSafeEqual(received, expected)

  if (!valid) {
    return res.status(401).send('Invalid webhook signature')
  }

  const event = JSON.parse(rawBody)

  if (event.event === 'message.received') {
    console.log('Inbound message:', event.message.text)
  }

  return res.sendStatus(204)
})

app.use(express.json())
app.listen(3000)
For replay protection, reject timestamps that are too old for your application and use X-ChatKazi-Delivery to ignore deliveries you have already processed.

Event types

ChatKazi currently sends these webhook event types:

EventSent when
message.receivedA connected WhatsApp session receives a message from a contact, group, channel, or newsletter.
webhook.testYou request a test delivery with POST /webhook/test.

ChatKazi does not currently send webhook events for outbound messages, message delivery receipts, session state changes, or QR updates.

Common event fields

Every webhook payload includes these top-level fields:

FieldTypeDescription
eventstringEvent type. Use this field to select the correct event handler.
timestampstringISO 8601 time when ChatKazi created the event.
customerIdstringChatKazi account that owns the session and webhook.
sessionIdstringConnected WhatsApp session that produced the event. Test events use default.

message.received

Sent when a connected WhatsApp session receives a message.

Messages sent by the connected WhatsApp account itself and WhatsApp status broadcasts are ignored.

Direct message example

message.received
{
  "event": "message.received",
  "timestamp": "2026-06-05T12:00:01.125Z",
  "customerId": "customer_id",
  "sessionId": "default",
  "message": {
    "id": "AC7BA799EE6E7A886B3C1BF59F57FDDD",
    "from": "254712345678@s.whatsapp.net",
    "replyTo": "254712345678@s.whatsapp.net",
    "phoneNumber": "254712345678",
    "chat": "194446171848909@lid",
    "participant": null,
    "rawFrom": "194446171848909@lid",
    "rawChat": "194446171848909@lid",
    "fromMe": false,
    "pushName": "Customer",
    "type": "conversation",
    "text": "Hello",
    "interaction": null,
    "media": null,
    "messageTimestamp": "2026-06-05T12:00:00.000Z"
  }
}

Group media message example

message.received group image
{
  "event": "message.received",
  "timestamp": "2026-06-05T12:05:01.125Z",
  "customerId": "customer_id",
  "sessionId": "support",
  "message": {
    "id": "A5376359CDF8B3B2D67FECB863393B54",
    "from": "254798765432@s.whatsapp.net",
    "replyTo": "120363333241980924@g.us",
    "phoneNumber": "254798765432",
    "chat": "120363333241980924@g.us",
    "participant": "247871504945317@lid",
    "rawFrom": "247871504945317@lid",
    "rawChat": "120363333241980924@g.us",
    "fromMe": false,
    "pushName": "Customer",
    "type": "imageMessage",
    "text": "Invoice attached",
    "interaction": null,
    "media": {
      "type": "image",
      "mimetype": "image/jpeg",
      "fileName": null,
      "caption": "Invoice attached",
      "seconds": null
    },
    "messageTimestamp": "2026-06-05T12:05:00.000Z"
  }
}

Message fields

FieldTypeDescription
idstring or nullWhatsApp message ID. Store this if you need to deduplicate events.
fromstring or nullBest available identifier for the sender. ChatKazi resolves WhatsApp LID identifiers to phone-number JIDs when possible.
replyTostring or nullUse this value as to when replying. It identifies the direct conversation or group where the reply belongs.
phoneNumberstring or nullSender phone number without +, when ChatKazi can resolve one. It can be null for some channels or unresolved identifiers.
chatstring or nullOriginal WhatsApp conversation identifier. For direct messages this can be a LID; for groups it ends with @g.us.
participantstring or nullOriginal participant identifier for a group message. Usually null for direct messages.
rawFromstring or nullOriginal sender identifier received from WhatsApp before phone-number resolution.
rawChatstring or nullOriginal conversation identifier received from WhatsApp. Preserve it for diagnostics, but do not normally use it when replying.
fromMebooleanWhether the connected WhatsApp account sent the message. Currently emitted inbound events have this set to false.
pushNamestring or nullSender display name supplied by WhatsApp.
typestringWhatsApp message content type, such as conversation, extendedTextMessage, imageMessage, or documentMessage.
textstringExtracted text, selected response, or media caption. Empty when the message has no supported text value.
interactionobject or nullStructured reply or list selection. Use interaction.id to route the selected action.
mediaobject or nullMedia metadata for supported media messages. ChatKazi does not include the media file itself.
messageTimestampstringISO 8601 time reported by WhatsApp for the original message.

Identifier fields

WhatsApp can represent a user with a phone-number JID such as 254712345678@s.whatsapp.net or an internal LID such as 194446171848909@lid.

GoalField to use
Reply to the messagemessage.replyTo
Identify the sendermessage.from
Read the sender phone numbermessage.phoneNumber
Determine the original conversationmessage.chat
Debug the identifiers received from WhatsAppmessage.rawFrom and message.rawChat
Always send replies using the event's sessionId and message.replyTo. Do not build a reply target from phoneNumber, from, chat, rawFrom, or rawChat.

For a direct message, replyTo is normally a resolved phone-number JID. For a group message, replyTo is the group JID so your response goes back to the group rather than privately to its participant.

Interactive response example

When a recipient selects a reply button or list row, the same message.received event includes a structured interaction object:

message.received interactive response
{
  "event": "message.received",
  "timestamp": "2026-06-07T12:00:01.125Z",
  "customerId": "customer_id",
  "sessionId": "default",
  "message": {
    "id": "3EB0F6BBD165A34B",
    "from": "254712345678@s.whatsapp.net",
    "replyTo": "254712345678@s.whatsapp.net",
    "phoneNumber": "254712345678",
    "chat": "254712345678@s.whatsapp.net",
    "participant": null,
    "rawFrom": "254712345678@s.whatsapp.net",
    "rawChat": "254712345678@s.whatsapp.net",
    "fromMe": false,
    "pushName": "Customer",
    "type": "interactiveResponseMessage",
    "text": "track_order",
    "interaction": {
      "type": "quick_reply",
      "id": "track_order",
      "label": null
    },
    "media": null,
    "messageTimestamp": "2026-06-07T12:00:00.000Z"
  }
}
Interaction fieldTypeDescription
typestringWhatsApp interaction type, such as quick_reply, list, or single_select.
idstring or nullStable action or row ID supplied when the interactive message was sent. Use this to select application behavior.
labelstring or nullDisplay label when WhatsApp includes it. Do not rely on it as a command ID.

Concurrent events and reply routing

Webhook deliveries can arrive close together or be processed concurrently. Keep the complete event together while processing it.

Use this pair from the same message.received event when replying:

Send-message fieldWebhook value
sessionIdTop-level event.sessionId
toevent.message.replyTo

Do not store either value in a shared global variable or select the most recently received sender. Queue each event with its own sessionId and replyTo.

Supplying both values from the same signed event ensures the reply uses the WhatsApp number that received the message and returns to the correct conversation.

Reply example

reply-to-webhook.js
async function replyToMessage(event) {
  const response = await fetch(`${process.env.CHATKAZI_BASE_URL}/messages/text`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': process.env.CHATKAZI_API_KEY
    },
    body: JSON.stringify({
      sessionId: event.sessionId,
      to: event.message.replyTo,
      text: 'Thanks, we received your message.'
    })
  })

  if (!response.ok) {
    throw new Error(`Reply failed with HTTP ${response.status}`)
  }
}

Never hardcode sessionId when replying to webhook events unless your account only has one session and it is guaranteed to remain that way.

Media fields

The media object is present for image, video, audio, document, and sticker messages.

FieldTypeDescription
typestringSimplified media type: image, video, audio, document, or sticker.
mimetypestring or nullMedia MIME type reported by WhatsApp.
fileNamestring or nullFile name reported for the media, commonly used for documents.
captionstring or nullCaption attached to an image, video, or document.
secondsnumber or nullDuration reported for audio or video media.

webhook.test

Sent when you request a test event. Use it to confirm that your destination is reachable and signature verification is configured correctly.

webhook.test
{
  "event": "webhook.test",
  "timestamp": "2026-06-05T12:10:00.000Z",
  "customerId": "customer_id",
  "sessionId": "default",
  "data": {
    "message": "This is a test event from ChatKazi."
  }
}

Test event fields

FieldTypeDescription
data.messagestringHuman-readable text identifying the payload as a ChatKazi test event.

Send a test event

POST /webhook/test
Terminal
curl -X POST "$CHATKAZI_BASE_URL/webhook/test" \
  -H "x-api-key: $CHATKAZI_API_KEY"

The test sends a signed webhook.test event to your enabled webhook URL.

Rotate the signing secret

Rotate the secret if it has been exposed or as part of your regular security process:

Terminal
curl -X PATCH "$CHATKAZI_BASE_URL/webhook" \
  -H "x-api-key: $CHATKAZI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/chatkazi",
    "enabled": true,
    "rotateSecret": true
  }'

The response returns the new secret once. Update your webhook endpoint before sending another test event.

Delivery behavior

  • ChatKazi considers any 2xx response successful.
  • Requests time out after 10 seconds.
  • Failed deliveries are recorded for inspection but are not automatically retried.
  • GET /webhook returns your configuration and the 20 most recent deliveries.
  • DELETE /webhook removes the configured destination.
Copyright © 2026