Logo
Search
API Docs

Webhook Signature Verification & Security

Webhook Reliability & Security

Webhook Signature Verification & Security

Overview

If your server URL receives call data from the core system, you should verify that each request genuinely came from the core system before trusting it. This page covers the two authentication methods available for that — HMAC signatures (recommended for maximum security) and Bearer Token (simpler to set up) — along with production security practices and the webhook response-timeout you need to work within. For the general credential-type overview and how credentials get referenced across assistants, phone numbers, and tools, see Credentials & Auth.


HMAC Signature Verification

HMAC authentication is the recommended, highest-security option: rather than just checking a shared secret, your server recomputes a cryptographic signature from the raw request body and confirms it matches the signature the core system sent. Create an HMAC credential in the dashboard with:

FieldDescription
Secret KeyYour HMAC secret key
AlgorithmHash algorithm (e.g. SHA256, SHA1)
Signature HeaderHeader name the signature is sent in (e.g. x-signature)
Timestamp HeaderOptional header for replay-attack protection

Reference the credential by ID in your server configuration:

{
  "server": {
    "url": "https://your-server.sulus.ai/webhook",
    "credentialId": "cred_hmac_456"
  }
}

Example Verification Handler: Node.js & Python

Node.js / Express:

import express from "express";
import crypto from "crypto";

const app = express();

app.use(
  express.json({
    verify: (req, res, buf) => {
      req.rawBody = buf;
    },
  })
);

const HMAC_SECRET = process.env.SULUS_HMAC_SECRET;

function verifySulusSignature(req, res, next) {
  const signature = req.headers["x-signature"];
  const timestamp = req.headers["x-webhook-timestamp"];

  if (!signature) {
    return res.status(401).json({ error: "Missing signature header" });
  }

  if (timestamp) {
    const requestTime = parseInt(timestamp, 10);
    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - requestTime) > 300) {
      return res.status(401).json({ error: "Request timestamp too old" });
    }
  }

  const expectedSignature = crypto
    .createHmac("sha256", HMAC_SECRET)
    .update(req.rawBody)
    .digest("hex");

  const sigBuffer = Buffer.from(signature, "hex");
  const expectedBuffer = Buffer.from(expectedSignature, "hex");

  if (
    sigBuffer.length !== expectedBuffer.length ||
    !crypto.timingSafeEqual(sigBuffer, expectedBuffer)
  ) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  next();
}

app.post("/webhook", verifySulusSignature, async (req, res) => {
  res.status(200).send();
});

Python / FastAPI:

import hmac
import hashlib
import time
import os
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()

HMAC_SECRET = os.getenv("SULUS_HMAC_SECRET")

async def verify_sulus_signature(request: Request) -> bytes:
    signature = request.headers.get("x-signature")
    timestamp = request.headers.get("x-webhook-timestamp")

    if not signature:
        raise HTTPException(status_code=401, detail="Missing signature header")

    if timestamp:
        request_time = int(timestamp)
        now = int(time.time())
        if abs(now - request_time) > 300:
            raise HTTPException(status_code=401, detail="Request timestamp too old")

    raw_body = await request.body()

    expected_signature = hmac.new(
        HMAC_SECRET.encode("utf-8"),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected_signature):
        raise HTTPException(status_code=401, detail="Invalid signature")

    return raw_body

@app.post("/webhook")
async def handle_webhook(request: Request):
    raw_body = await verify_sulus_signature(request)
    return {"status": "ok"}

Always compute the signature from the raw request body — parsing JSON first can reorder bytes and break the match — and use a constant-time comparison (crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python) to avoid timing attacks.


Bearer Token Authentication

Bearer Token is the simpler alternative: a token sent in a request header, without a computed signature. Configure a Bearer Token credential with a token, a header name (default Authorization), and whether to include the "Bearer " prefix. Your server then receives requests like:

POST /webhook HTTP/1.1
Host: your-server.sulus.ai
Authorization: Bearer your-api-token-here
Content-Type: application/json

For backward compatibility with older inline-secret setups, you can instead configure the header name as X-Sulus-Secret with the Bearer prefix disabled — this replicates the legacy inline secret field behavior.


Production Security Best Practices

  • Never log sensitive data — request bodies, secrets, or tokens should never end up in plaintext logs.
  • Validate the signature (or token) on every request, not just some — don't special-case any endpoint as exempt.
  • Use HTTPS for every production webhook endpoint.
  • Handle verification failures without leaking internals — return a generic 401, not details about why the signature didn't match.
  • Monitor webhook delivery in the dashboard so you notice failures quickly rather than discovering them from a customer complaint.

Development-Only Tools & Response Timeout

A local CLI listener/tunnel setup is convenient for developing and testing your webhook handler locally, but it's for development only — never point production traffic through a local tunnel.

Separately, your webhook handler needs to respond within 10 seconds. If verification and processing together would take longer than that, verify the signature first, then queue the heavy work for background processing and acknowledge with an HTTP 200 immediately — don't make the core system wait on a slow downstream call before you've even confirmed the request is legitimate.