Calendly webhook signature t= v1= with 3 minutes vs Sume sume-v1

Calendly sends t=<ts>,v1=<sig> signed over t.body and suggests 3 minutes of tolerance. Sume signs timestamp.body as sume-v1; write a separate check for each.

4 min readSume
All posts

Calendly puts its timestamp and signature in one header, Calendly-Webhook-Signature: t=<ts>,v1=<sig>, and signs t + "." + body with HMAC-SHA256. It also recommends a tolerance window of 3 minutes. Sume sends separate headers and a sume-v1=<hex> signature over <timestamp>.<raw_body>. The two are close, but a parser for one will not read the other.

Compare the two

The signed string is the same shape. The packaging is not.

Calendly and Sume webhook signatures (vendor docs, read 2026-10-05)
DetailCalendlySume
Header layoutOne header: t=<ts>,v1=<sig>Separate timestamp and signature headers
Signed stringt + "." + body<timestamp>.<raw_body>
AlgorithmHMAC-SHA256HMAC-SHA256
Replay windowRecommends 3 minutes toleranceSDK verifyWebhook: toleranceSeconds, default 300
RotationNot covered hereComma-separated entries, newest first, 24 hour overlap

A Calendly check

Split the header on commas, read t and v1, reject a stale timestamp, then compare the digest. Use the raw bytes of the body.

import hashlib, hmac, os, time

def calendly_ok(raw: bytes, header: str, tolerance: int = 180) -> bool:
    secret = os.environ["CALENDLY_SIGNING_KEY"]
    if not secret:
        raise RuntimeError("empty secret")
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, sig = parts["t"], parts["v1"]
    if abs(time.time() - int(t) / 1000) > tolerance:
        return False
    mac = hmac.new(secret.encode(), (t + ".").encode() + raw, hashlib.sha256)
    return hmac.compare_digest(mac.hexdigest(), sig)

Check the unit of the timestamp

The example treats t as milliseconds, which is an assumption (Sume's own timestamp is epoch seconds, so do not copy its unit) to confirm against a real delivery. Calendly's page defines the format; read it and log one sample header before you trust the window. If you get the unit wrong, every request looks stale or none do.

Booking to a music job

Reading the vendor page again when you build is worth the ten minutes, because signature formats change less often than they get documented badly. A common use is a booking that triggers a Sume Music Router job, for example a short intro bed for a client call recap. Keep the job start behind a queue, and key it on the Calendly event identifier with Idempotency-Key, so a retry never pays the fixed $0.125 twice. Sume's own completion webhook then needs its own verifier, as above.

Before you ship

  • Reject signatures older than your tolerance window.
  • Store the Calendly event uuid to dedupe.
  • Use a separate secret for each vendor.
  • Return 2xx quickly, then start the Sume job.
  • Treat a missing or malformed header as a 401, not a 500.
  • Re-read the vendor page when you build; do not rely on this table alone.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume