From tawk
Use when receiving tawk.to webhook events (chat start/end, transcripts, new tickets) — scaffolds a receiver endpoint with HMAC-SHA1 signature verification for Express, Cloudflare Workers, or Next.js.
How this skill is triggered — by the user, by Claude, or both
Slash command
/tawk:webhook-receiverThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Reference: https://developer.tawk.to/webhooks/
Reference: https://developer.tawk.to/webhooks/
chat:start, chat:end, chat:transcript (sent ~3 minutes after the chat
ends), ticket:create.X-Tawk-Signature header = HMAC-SHA1 (hex) of the raw
request body using the webhook secret. ALWAYS verify it — never scaffold without.X-Hook-Event-Id is identical
across retries — use it for idempotency.Verify against the RAW body bytes, not re-serialized JSON. Use a constant-time compare.
const crypto = require("crypto");
function verifyTawkSignature(rawBody, signatureHeader, secret) {
const expected = crypto.createHmac("sha1", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(signatureHeader ?? "", "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
express.json() destroys the raw body — capture it with express.raw on this route:
const express = require("express");
const app = express();
app.post("/webhooks/tawk", express.raw({ type: "application/json" }), (req, res) => {
if (!verifyTawkSignature(req.body, req.get("X-Tawk-Signature"), process.env.TAWK_WEBHOOK_SECRET)) {
return res.status(401).send("bad signature");
}
const event = JSON.parse(req.body.toString("utf8"));
res.sendStatus(200); // ack immediately, process after
switch (event.event) {
case "chat:start": /* handle */ break;
case "chat:end": /* handle */ break;
case "chat:transcript": /* handle */ break;
case "ticket:create": /* handle */ break;
}
});
// app/api/webhooks/tawk/route.ts
import crypto from "crypto";
export async function POST(request: Request) {
const rawBody = Buffer.from(await request.arrayBuffer());
const signature = request.headers.get("x-tawk-signature") ?? "";
const expected = crypto
.createHmac("sha1", process.env.TAWK_WEBHOOK_SECRET!)
.update(rawBody)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(signature, "hex");
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return new Response("bad signature", { status: 401 });
}
const event = JSON.parse(rawBody.toString("utf8"));
// dispatch on event.event as in the Express example
return new Response("ok");
}
WebCrypto has no SHA-1 HMAC shortage — use crypto.subtle:
async function verifyTawkSignature(rawBody, signatureHex, secret) {
const key = await crypto.subtle.importKey(
"raw", new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-1" }, false, ["sign"]
);
const mac = await crypto.subtle.sign("HMAC", key, rawBody);
const expected = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, "0")).join("");
return expected === signatureHex; // acceptable here; timingSafeEqual unavailable in Workers
}
export default {
async fetch(request, env) {
if (request.method !== "POST") return new Response("not found", { status: 404 });
const rawBody = await request.arrayBuffer();
const ok = await verifyTawkSignature(rawBody, request.headers.get("X-Tawk-Signature"), env.TAWK_WEBHOOK_SECRET);
if (!ok) return new Response("bad signature", { status: 401 });
const event = JSON.parse(new TextDecoder().decode(rawBody));
// dispatch on event.event
return new Response("ok");
},
};
TAWK_WEBHOOK_SECRET) — never hardcode.X-Hook-Event-Id if their downstream effects aren't naturally
idempotent.Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub beyrakin/tawk-plugin --plugin tawk