Agent skill
Chatim Webhooks Skill
Receive and verify Chatim webhooks. Use when setting up Chatim chat widget webhook handlers, debugging X-Chatim-Signature verification, or handling events like chat.started, chat.message.received, chat.form.submitted, chat.closed, chat.assigned, and chat.handoff.
Install this skill
npx skills add hookdeck/webhook-skills --skill chatim-webhooks
When to Use This Skill
- How do I receive Chatim webhooks?
- How do I verify the
X-Chatim-Signatureheader? - How do I handle
chat.started,chat.form.submitted, orchat.closedevents? - Why is my Chatim webhook signature verification failing?
- How do I send new chat leads from a Chatim widget to my CRM?
Essential Code (USE THIS)
Chatim signs every delivery with two headers:
X-Chatim-Signature: sha256=<hex>
X-Chatim-Timestamp: 1736937000
- The signature is HMAC-SHA256 (lowercase hex) of
<X-Chatim-Timestamp value>.<raw body>, keyed with the webhook secret (shown once when you create the webhook in the dashboard), used as-is (UTF-8, not decoded). - Accept the digest with or without the
sha256=prefix, and compare hex case-insensitively. - The event name is in the body's
eventfield (also sent inX-Chatim-Event).X-Chatim-Delivery-Idand the body'seventIdare unique per delivery. - Answer a missing, malformed or wrong signature with
401. Chatim treats any4xxas a permanent failure and does not retry, so don't return one for anything else.
Verification (Node.js)
const crypto = require('crypto');
// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifyChatim(rawBody, signatureHeader, timestampHeader, secret) {
if (!secret || !signatureHeader || !timestampHeader) return false;
const hex = signatureHeader.startsWith('sha256=') ? signatureHeader.slice(7) : signatureHeader;
if (!/^[0-9a-fA-F]{64}$/.test(hex)) return false; // validate before decoding
const message = Buffer.concat([Buffer.from(timestampHeader, 'latin1'), Buffer.from('.'), rawBody]);
const expected = crypto.createHmac('sha256', secret).update(message).digest();
const received = Buffer.from(hex, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
Verification (Python)
import hashlib, hmac, re
def verify_chatim(raw_body: bytes, signature_header, timestamp_header, secret: str) -> bool:
if not secret or not signature_header or not timestamp_header:
return False
digest = signature_header.removeprefix("sha256=")
if not re.fullmatch(r"[0-9a-fA-F]{64}", digest): # validate before decoding
return False
message = b"".join([timestamp_header.encode("latin-1"), b".", raw_body])
expected = hmac.new(secret.encode(), message, hashlib.sha256).digest()
return hmac.compare_digest(bytes.fromhex(digest), expected)
Chatim's own docs reject timestamps older than 5 minutes, and their Node sample signs JSON.stringify(req.body). The examples differ on both points:
- They sign the raw body bytes (as Chatim's troubleshooting section says). Re-serialized JSON can differ from what was sent.
- The timestamp check is off by default, because retries and replays (Chatim's and Hookdeck's) resend the original
X-Chatim-Timestamp. SetCHATIM_to a positive number of seconds (Chatim's examples useWEBHOOK_ TOLERANCE_ SECONDS 300) to opt in. UseeventIdfor idempotency instead.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Common Event Types
Dispatch on the body's event field.
| Event | Triggered When |
|---|---|
chat.started | A visitor opens the widget and begins a new session |
chat.message.received | A visitor sends a text message |
chat.form.submitted | A visitor submits a form in the chatbot flow |
chat.closed | A chat session is closed (by agent, visitor, system or timeout) |
chat.assigned | A support agent joins the conversation |
chat.handoff | The chatbot transitions from bot mode to live chat |
For payload shapes and the full list, see references/overview.md and Chatim's webhook docs.
Important Headers
| Header | Description |
|---|---|
X-Chatim-Signature | sha256=<hex> HMAC-SHA256 signature |
X-Chatim-Timestamp | Unix timestamp (seconds) of when the request was sent |
X-Chatim-Event | The event type |
X-Chatim-Delivery-Id | Unique delivery identifier |
Environment Variables
CHATIM_WEBHOOK_SECRET=your_webhook_secret # shown once when the webhook is created
CHATIM_WEBHOOK_TOLERANCE_SECONDS=0 # optional; a positive value opts in to the replay window
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 chatim --path /webhooks/chatim
Reference Materials
- references/overview.md - Chatim webhook concepts, events, payloads, retries
- references/setup.md - Creating a webhook and getting the secret
- references/verification.md - Signature verification details and gotchas