Skip to main content
CoReceptionist
Resources / Knowledge Base / Integrations / Verify webhook signatures
INTEGRATIONS · 4 MIN · UPDATED OCT 2026

Verify webhook signatures

Anyone who learns your webhook URL could send it fake requests. Every request CoReceptionist sends is signed with your webhook's secret, so your system can reject anything that isn't.

1
How the signature worksThe X-CoReceptionist-Signature header looks like t=1791869117,v1=5f2c… where t is the time the request was sent (Unix seconds) and v1 is an HMAC-SHA256, in hex, of the text t + "." + the raw request body, using your signing secret as the key. Recompute it from the raw body exactly as received — before any JSON parsing — and compare.
2
Node.js (Express)Keep the raw body, verify, then parse.
server.js
const crypto = require("crypto");
const express = require("express");
const app = express();
const SECRET = process.env.COREC_WEBHOOK_SECRET; // whsec_…

app.post("/coreceptionist", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("X-CoReceptionist-Signature") || "";
  const { t, v1 } = Object.fromEntries(header.split(",").map((part) => part.split("=")));
  const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${req.body}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) <= 300;
  const valid = v1 && v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
  if (!fresh || !valid) return res.status(400).send("invalid signature");

  res.sendStatus(200); // reply first, then do the work
  const event = JSON.parse(req.body);
  console.log(event.type, event.data.visitor.full_name);
});

app.listen(3000);
3
Python (Flask)The same check in Python.
app.py
import hashlib, hmac, os, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["COREC_WEBHOOK_SECRET"].encode()  # whsec_…

@app.post("/coreceptionist")
def coreceptionist():
    raw = request.get_data()  # raw bytes, before parsing
    parts = dict(p.split("=", 1) for p in request.headers.get("X-CoReceptionist-Signature", "").split(",") if "=" in p)
    t, v1 = parts.get("t", ""), parts.get("v1", "")
    expected = hmac.new(SECRET, f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
    if not t.isdigit() or abs(time.time() - int(t)) > 300 or not hmac.compare_digest(expected, v1):
        abort(400)
    event = request.get_json()
    print(event["type"], event["data"]["visitor"]["full_name"])
    return "", 200
4
Reject old and repeated requestsRefuse requests whose timestamp is more than 5 minutes from your clock, so a captured request can't be replayed later. Keep the X-CoReceptionist-Delivery values you have processed for a day and skip repeats — a retry after a slow reply can deliver the same event twice.
5
Replace the secret if it leaksOn the Webhooks page choose ⋮ → Replace signing secret. The old secret stops working immediately, so update your system with the new one straight away. Each replacement and every time someone shows the secret is recorded.
TIPComparing signatures with a constant-time function (timingSafeEqual, hmac.compare_digest) rather than == stops attackers guessing the signature one character at a time.
Was this helpful?
Related articlesWebhook payload referenceIntegrations →Set up webhooks (Beta)Integrations →

Still stuck? Talk to a human