Signature Verification
Verify webhook authenticity with HMAC-SHA256 signatures. Python and Node.js examples.
Overview
Every webhook from CueAPI includes an HMAC-SHA256 signature in the X-CueAPI-Signature header. Verify this signature to ensure the webhook is authentic and hasn't been tampered with.
How it works
- CueAPI creates a signed message:
"{timestamp}.{json_payload}" - Signs it with your per-user webhook secret using HMAC-SHA256
- Sends the signature as
X-CueAPI-Signature: v1={hex_digest} - Sends the timestamp as
X-CueAPI-Timestamp: {unix_epoch}
Your handler:
- Extracts the timestamp and signature from headers
- Checks the timestamp is within 5 minutes (replay protection)
- Reconstructs the signed message and recomputes the HMAC
- Compares using constant-time comparison
Get your webhook secret
curl https://api.cueapi.ai/v1/auth/webhook-secret \
-H "Authorization: Bearer cue_sk_..."Your secret looks like whsec_a1b2c3d4... (64 hex characters after the prefix).
Python verification
import hashlib
import hmac
import json
import time
def verify_webhook(payload_bytes: bytes, secret: str,
signature: str, timestamp: str,
tolerance: int = 300) -> bool:
"""Verify a CueAPI webhook signature.
Args:
payload_bytes: Raw request body bytes
secret: Your webhook secret (whsec_...)
signature: X-CueAPI-Signature header value
timestamp: X-CueAPI-Timestamp header value
tolerance: Max age in seconds (default 5 min)
"""
# 1. Check timestamp freshness (replay protection)
try:
ts = int(timestamp)
except (ValueError, TypeError):
return False
if abs(time.time() - ts) > tolerance:
return False
# 2. Reconstruct signed content
signed_content = f"{timestamp}.".encode("utf-8") + payload_bytes
# 3. Compute expected signature
expected = hmac.new(
secret.encode("utf-8"),
signed_content,
hashlib.sha256
).hexdigest()
# 4. Compare (constant-time)
return hmac.compare_digest(f"v1={expected}", signature)Flask example
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_secret_here"
@app.route("/webhook", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-CueAPI-Signature")
timestamp = request.headers.get("X-CueAPI-Timestamp")
if not signature or not timestamp:
abort(401)
if not verify_webhook(
request.get_data(),
WEBHOOK_SECRET,
signature,
timestamp
):
abort(401)
# Signature verified — process the webhook
payload = request.json
process(payload)
return {"status": "ok"}, 200Node.js verification
const crypto = require('crypto');
function verifyWebhook(payloadString, secret, signature, timestamp, tolerance = 300) {
// 1. Check timestamp freshness
const ts = parseInt(timestamp, 10);
if (isNaN(ts) || Math.abs(Date.now() / 1000 - ts) > tolerance) {
return false;
}
// 2. Reconstruct signed content
const signedContent = `${timestamp}.${payloadString}`;
// 3. Compute expected signature
const expected = crypto
.createHmac('sha256', secret)
.update(signedContent)
.digest('hex');
// 4. Compare (constant-time)
const expectedSig = `v1=${expected}`;
return crypto.timingSafeEqual(
Buffer.from(expectedSig),
Buffer.from(signature)
);
}Express example
const express = require('express');
const app = express();
const WEBHOOK_SECRET = 'whsec_your_secret_here';
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-cueapi-signature'];
const timestamp = req.headers['x-cueapi-timestamp'];
if (!signature || !timestamp) {
return res.status(401).json({error: 'Missing signature headers'});
}
if (!verifyWebhook(req.body.toString(), WEBHOOK_SECRET, signature, timestamp)) {
return res.status(401).json({error: 'Invalid signature'});
}
// Signature verified
const payload = JSON.parse(req.body);
process(payload);
res.json({status: 'ok'});
});Important notes
Warning
Use raw body bytes for verification. The signature is computed over the exact JSON bytes CueAPI sent. If your framework parses and re-serializes the JSON, the bytes may differ and verification will fail.
Note
Timestamp tolerance. The default 5-minute window protects against replay attacks while allowing for reasonable clock skew and network delays.
Rotating your secret
If your webhook secret is compromised, regenerate it immediately:
curl -X POST https://api.cueapi.ai/v1/auth/webhook-secret/regenerate \
-H "Authorization: Bearer cue_sk_..."The old secret is immediately revoked. Update your handler before regenerating.