Skip to content

Webhook HMAC verification

Every outbound webhook delivery from Chatevo is signed with HMAC-SHA256 so your endpoint can verify authenticity before processing.

Each POST to your webhook URL includes:

HeaderDescription
X-Chatevo-SignatureHex-encoded HMAC-SHA256 signature
X-Chatevo-TimestampUnix timestamp (seconds) when the payload was signed
X-Chatevo-EventEvent type, e.g. message.created
Content-Typeapplication/json

The signed string is:

{timestamp}.{raw_request_body}

HMAC key: your endpoint’s signing_secret (returned once at webhook creation).

signature = HMAC_SHA256(signing_secret, "{timestamp}.{body}")
  1. Read the raw request body (before JSON parsing).
  2. Read X-Chatevo-Timestamp and reject if older than 5 minutes (replay protection).
  3. Compute expected signature from timestamp + "." + body.
  4. Compare with X-Chatevo-Signature using constant-time comparison.
  5. Process the event only if signatures match.
const crypto = require("crypto");
function verifyChatevoWebhook(req, signingSecret) {
const signature = req.headers["x-chatevo-signature"];
const timestamp = req.headers["x-chatevo-timestamp"];
const body = req.rawBody; // use raw bytes, not re-serialized JSON
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (age > 300) return false;
const expected = crypto
.createHmac("sha256", signingSecret)
.update(`${timestamp}.${body}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
}
import hmac
import hashlib
import time
def verify(signing_secret: str, timestamp: str, body: bytes, signature: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
payload = f"{timestamp}.".encode() + body
expected = hmac.new(
signing_secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
  1. Create a new webhook endpoint (or PATCH to rotate secret).
  2. Update your verifier with the new secret.
  3. Delete the old endpoint after traffic drains.

Log and return 401 — do not process the payload. Legitimate retries use the same signing scheme.