Gareth Wilson Gareth Wilson

Guide to Chatim Webhooks: Features and Best Practices

Published


Chatim webhooks tell your systems what is happening in your website chat: a visitor opens the widget, sends a message, fills in a form in a chatbot flow, or gets handed to a human agent. If you run live chat or chatbots with Chatim, webhooks are how those conversations and leads reach your CRM, your team's chat tools or your analytics as they happen.

This guide covers how Chatim webhooks are created and signed, the six chat events and their payloads, Chatim's retry and auto-disable rules, and the practices that keep a Chatim receiver reliable.

What are Chatim webhooks?

A Chatim webhook belongs to a project. You give it a URL and pick the events it should receive, and Chatim sends each event as an HTTP POST with a JSON body. Every body carries the event name, a unique eventId, the project and chat UUIDs, and an event-specific data object.

Each request is signed. Chatim computes an HMAC-SHA256 over the X-Chatim-Timestamp header, a dot and the payload, keyed with the webhook's secret, and sends it in X-Chatim-Signature as sha256=<hex>. Chatim shows the secret once, straight after you create the webhook.

Chatim webhook features

FeatureDetails
ConfigurationDashboard: select the project, then Webhooks > Create Webhook
Subscription modelOne or more events per webhook
Signature headerX-Chatim-Signature: sha256=<hex>
Timestamp headerX-Chatim-Timestamp, Unix seconds
Other headersX-Chatim-Event (the event type), X-Chatim-Delivery-Id (a unique delivery ID)
SchemeHMAC-SHA256, hex
Signed content<X-Chatim-Timestamp>.<raw body>
SecretPer webhook, shown once. Regenerate Secret invalidates the old one immediately
Response window30 seconds
Retries5xx and timeouts: after 1 minute, 5 minutes, 30 minutes, 2 hours and 24 hours. 4xx: none
Auto-disableAfter 10 consecutive failures
TestingTest sends a sample chat.started
Delivery historyKept for 30 days
Source IPsNot published

Common events

EventFires when
chat.startedA visitor opens the chat widget and begins a new session
chat.message.receivedA visitor sends a text message
chat.form.submittedA visitor submits a form in the chatbot flow
chat.closedA chat session is closed
chat.assignedA support agent joins the conversation
chat.handoffThe chatbot hands the visitor over from bot mode to live chat

Every delivery shares these common fields:

{
  "event": "chat.started",
  "eventId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2025-01-15T10:30:00.000Z",
  "projectUuid": "your-project-uuid",
  "chatUuid": "visitor-chat-uuid",
  "data": { }
}

The data object depends on the event. Chatim's chat.form.submitted example, which is the one most integrations want for lead capture, looks like this:

{
  "event": "chat.form.submitted",
  "data": {
    "formNodeId": "node-id",
    "formName": "Contact Form",
    "fields": {
      "name": "John Doe",
      "phone": "+1234567890",
      "message": "I'd like a product demo."
    },
    "visitor": { "uuid": "visitor-uuid", "name": "John Doe" }
  }
}

chat.started carries the visitor, the page URL and title, a location and a parsed user agent. chat.closed carries closedBy (agent, visitor, system or timeout), duration in seconds and messageCount. Chatim's description of chat.closed also mentions a transcript, which its example doesn't show. The per-event examples leave out the common fields, so expect both together in a real delivery.

Setting up Chatim webhooks

  1. Log in to the Chatim dashboard, select your project in the sidebar, and click Webhooks.
  2. Click Create Webhook, give it an optional name, and enter your HTTPS endpoint URL.
  3. Select the events you want and click Create.
  4. Copy the Webhook Secret that appears and store it server-side, for example as CHATIM_WEBHOOK_SECRET. Chatim shows it only once.
  5. Click Test to send a sample chat.started to your endpoint.

If the secret is compromised, click Regenerate Secret. The old one stops working immediately, so update your receiver at the same time.

If you receive Chatim webhooks through Hookdeck, create a source with the Chatim type, use its URL as the webhook URL, and paste the Webhook Secret into the source. The source accepts POST only, which is the method Chatim uses.

Securing Chatim webhooks

Read X-Chatim-Timestamp and X-Chatim-Signature. Build the signed bytes from the timestamp exactly as received, a literal ., and the raw request body. Compute an HMAC-SHA256 of them with the secret as a UTF-8 key, hex-encode it, and compare it with the digest after sha256= in constant time.

const crypto = require('crypto');
const express = require('express');

function verifyChatimSignature(rawBody, signatureHeader, timestampHeader, secret, toleranceSeconds = 0) {
  if (!signatureHeader || !timestampHeader || !secret) return false;

  // The header is "sha256=<hex>". Accept a bare digest too.
  const digest = signatureHeader.trim().replace(/^sha256=/, '');
  if (!/^[0-9a-fA-F]{64}$/.test(digest)) return false; // check before Buffer.from(x, 'hex')

  // Optional replay window, off by default. Retries and replays resend the original timestamp.
  if (toleranceSeconds > 0) {
    if (!/^\d+$/.test(timestampHeader)) return false;
    if (Math.abs(Date.now() / 1000 - Number(timestampHeader)) > toleranceSeconds) return false;
  }

  const expected = crypto
    .createHmac('sha256', secret)
    .update(Buffer.concat([Buffer.from(`${timestampHeader}.`, 'utf8'), rawBody]))
    .digest();
  const received = Buffer.from(digest, 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

const app = express();

app.post('/webhooks/chatim', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.CHATIM_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  const valid = verifyChatimSignature(
    req.body,
    req.get('x-chatim-signature'),
    req.get('x-chatim-timestamp'),
    secret
  );
  if (!valid) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on event.eventId, route on event.event, then process asynchronously.
  res.sendStatus(200);
});

The same check in Python:

import hashlib
import hmac
import re
import time


def verify_chatim_signature(raw_body: bytes, signature_header, timestamp_header, secret: str,
                            tolerance_seconds: int = 0) -> bool:
    if not signature_header or not timestamp_header or not secret:
        return False
    digest = signature_header.strip().removeprefix("sha256=")
    if not re.fullmatch(r"[0-9a-fA-F]{64}", digest):
        return False
    # Optional replay window, off by default.
    if tolerance_seconds > 0:
        if not (timestamp_header.isascii() and timestamp_header.isdigit()):
            return False
        if abs(time.time() - int(timestamp_header)) > tolerance_seconds:
            return False
    signed = timestamp_header.encode("utf-8") + b"." + raw_body
    expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(digest.lower().encode("utf-8"), expected.encode("utf-8"))


# In FastAPI: raw_body = await request.body()
# verify_chatim_signature(raw_body, request.headers.get("x-chatim-signature"),
#                         request.headers.get("x-chatim-timestamp"),
#                         os.environ["CHATIM_WEBHOOK_SECRET"])

Chatim's own Node example signs JSON.stringify(req.body), which is the body after a JSON parser has run, and it calls crypto.timingSafeEqual without a length check. Its troubleshooting section says the opposite about the body: sign timestamp + "." + raw_body, "not parsed JSON". Use the raw body. Chatim's Python example puts body into an f-string, which only works if body is already a decoded string. Pass bytes and you sign the b'...' representation instead.

Both of Chatim's examples reject requests older than 5 minutes, and its best practices say to validate the timestamp. The samples above leave that off unless you pass a tolerance. Hookdeck applies no timestamp tolerance, and its retries and replays resend Chatim's original X-Chatim-Timestamp, so a 5-minute window in your own code would reject a legitimate retry that arrives later. X-Chatim-Event isn't part of the signed content, so route on the verified body's event field rather than on the header.

Make Chatim webhooks production-ready. Hookdeck Event Gateway verifies the X-Chatim-Signature header over the timestamp and raw body, stores every chat and lead event, and delivers it to your service with retries you control.

Chatim webhook limitations and pain points

A 4xx is never retried

The Problem: Events disappear after a deploy that briefly returned 401 or 404.

Why It Happens: Chatim treats a 4xx response as a permanent failure and doesn't retry it. Only 5xx responses and timeouts are retried. A misconfigured secret, a moved route or a validation error all lose the event for good.

Workarounds:

  • Return a 5xx, not a 4xx, for failures that are on your side and temporary, so Chatim retries them.
  • Watch the delivery history in the dashboard after each deploy.

How Hookdeck Can Help: Hookdeck answers Chatim with a 200 as soon as it has stored the request, so your own 4xx responses never reach Chatim. Hookdeck then delivers to your service with retries you configure, and you can replay anything that failed. A request that fails signature verification is recorded as failed verification and doesn't reach your destination.

Ten failures switch the webhook off

The Problem: After a short outage, Chatim stops sending events entirely.

Why It Happens: A webhook that accumulates 10 consecutive failures is disabled automatically, and stays off until someone clicks Enable in the dashboard.

Workarounds:

  • Respond within the 30-second window and keep the handler light.
  • Check the webhook's Active state after incidents, and look at the disabled reason the dashboard shows.

How Hookdeck Can Help: Hookdeck accepts each delivery as it arrives, so your downtime doesn't count against Chatim's failure limit.

The sample signs re-serialised JSON

The Problem: Signatures match in some tests and fail on real traffic, especially messages with special characters.

Why It Happens: Chatim's Node example signs JSON.stringify(req.body). That only reproduces Chatim's bytes if your parser and serialiser happen to produce the same JSON.

Workarounds:

  • Mount express.raw() on the webhook route and sign the bytes you received.
  • Parse JSON only after verification.

How Hookdeck Can Help: Hookdeck's Chatim source verifies against the raw body it received, so the signature check is independent of whatever your framework does to the body.

Rotating the secret has no overlap

The Problem: Requests fail verification between regenerating the secret and deploying the new one.

Why It Happens: Regenerate Secret invalidates the old secret at once.

Workarounds:

  • Regenerate only when you can deploy the new secret immediately.
  • Remember that during the gap, your 401s are 4xx failures that Chatim won't retry.

How Hookdeck Can Help: With Hookdeck in front, the secret lives in the Chatim source. Update it there as you regenerate, and requests that failed in between are still stored.

Best practices

Verify against the raw body

The signature covers the exact bytes Chatim sent. In Express, use express.raw({ type: '*/*' }). In Next.js, use Buffer.from(await request.arrayBuffer()). In FastAPI, use await request.body().

Deduplicate on eventId

Retries resend the same event, and Chatim recommends idempotency based on eventId. Store each eventId before processing and skip repeats. See our guide to webhook idempotency.

Choose your status codes deliberately

Return 200 once the event is stored. For temporary failures on your side, return a 5xx so Chatim retries. Keep 4xx responses for requests you never want again, such as ones that fail verification.

Acknowledge fast and process asynchronously

Chatim waits 30 seconds. Verify, persist the event, return 200, and push CRM or Slack calls to a background job. See why to process webhooks asynchronously.

Conclusion

Chatim signs every webhook with a hex HMAC-SHA256 of the timestamp, a dot and the raw body, sent as sha256=<hex> in X-Chatim-Signature. Save the secret when it is shown, verify the raw bytes rather than re-serialised JSON, deduplicate on eventId, and be careful with 4xx responses, which Chatim never retries.

Hookdeck Event Gateway sits in front of your receiver, verifies every Chatim signature, and holds each chat event until your service has processed it, so your own errors don't cost you events or a disabled webhook.

Get started with Hookdeck for free and handle Chatim webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.