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

  1. CueAPI creates a signed message: "{timestamp}.{json_payload}"
  2. Signs it with your per-user webhook secret using HMAC-SHA256
  3. Sends the signature as X-CueAPI-Signature: v1={hex_digest}
  4. Sends the timestamp as X-CueAPI-Timestamp: {unix_epoch}

Your handler:

  1. Extracts the timestamp and signature from headers
  2. Checks the timestamp is within 5 minutes (replay protection)
  3. Reconstructs the signed message and recomputes the HMAC
  4. Compares using constant-time comparison

Get your webhook secret

bash
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

python
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

python
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"}, 200

Node.js verification

javascript
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

javascript
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:

bash
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.