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.

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.
| Detail | Calendly | Sume |
|---|---|---|
| Header layout | One header: t=<ts>,v1=<sig> | Separate timestamp and signature headers |
| Signed string | t + "." + body | <timestamp>.<raw_body> |
| Algorithm | HMAC-SHA256 | HMAC-SHA256 |
| Replay window | Recommends 3 minutes tolerance | SDK verifyWebhook: toleranceSeconds, default 300 |
| Rotation | Not covered here | Comma-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
- No generate_video tool for Sume in ChatGPT or Claude: Write is off
A Sume OAuth session with only mcp:read hides write and paid tools like generate_video. Reconnect with Write on, or use an API key; confirm in tools_list.
- Claude API MCP connector: public server, tool calls only, one toolset
The Claude API MCP connector reaches only public HTTP servers and supports only tool calls. mcp.sume.com fits. Here are the request, beta header and limits.
- Claude's MCP connector is not ZDR-eligible; what that means for Sume
Claude's MCP connector is not ZDR eligible and is unavailable on Bedrock and Google Cloud. For those cases, call Sume's REST API from your own backend.
- Claude MCP connector: 2025-04-04 beta header is deprecated
Claude's connector page shows mcp-client-2025-11-20 and marks 2025-04-04 deprecated; the release notes name mcp-client-2026-09-15. State both.
Written by Sume