Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.signa.so/llms.txt

Use this file to discover all available pages before exploring further.

TypeScript / Node — SDK helper

The @signa-so/sdk package exports a thin wrapper over standardwebhooks:
import express from 'express';
import { verifyWebhookSignature } from '@signa-so/sdk';

const app = express();
const SECRET = process.env.SIGNA_WEBHOOK_SECRET!;

app.post(
  '/signa-webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const ok = verifyWebhookSignature(
      {
        'webhook-id': req.header('webhook-id')!,
        'webhook-timestamp': req.header('webhook-timestamp')!,
        'webhook-signature': req.header('webhook-signature')!,
      },
      req.body.toString('utf-8'),
      SECRET,
    );
    if (!ok) return res.sendStatus(401);

    const event = JSON.parse(req.body.toString('utf-8'));
    handle(event);
    res.sendStatus(200);
  },
);
Behaviour:
  • Returns true for a valid signature against SECRET.
  • Returns false on stale timestamps (>5 min skew, enforced by the reference library).
  • Accepts rotation overlap (v1,<curr> v1,<prev>) — verifies if either entry passes.
The body MUST be the raw request bytes — no JSON.parse round-trip first. Mismatched whitespace breaks the HMAC.

TypeScript / Node — without the SDK

import { Webhook } from 'standardwebhooks';

const wh = new Webhook(process.env.SIGNA_WEBHOOK_SECRET!);
try {
  wh.verify(rawBody, {
    'webhook-id': req.header('webhook-id')!,
    'webhook-timestamp': req.header('webhook-timestamp')!,
    'webhook-signature': req.header('webhook-signature')!,
  });
} catch (err) {
  return res.sendStatus(401);
}

Python

Install standardwebhooks:
pip install standardwebhooks
from flask import Flask, request, abort
from standardwebhooks import Webhook, WebhookVerificationError
import os

app = Flask(__name__)
SECRET = os.environ["SIGNA_WEBHOOK_SECRET"]

@app.post("/signa-webhook")
def signa_webhook():
    raw_body = request.get_data(as_text=False)
    headers = {
        "webhook-id": request.headers["webhook-id"],
        "webhook-timestamp": request.headers["webhook-timestamp"],
        "webhook-signature": request.headers["webhook-signature"],
    }
    try:
        Webhook(SECRET).verify(raw_body, headers)
    except WebhookVerificationError:
        abort(401)

    event = request.get_json()
    handle(event)
    return "", 200

Go

import "github.com/standard-webhooks/standard-webhooks/libraries/go/standardwebhooks"

wh, err := standardwebhooks.NewWebhook(secret)
if err != nil { /* ... */ }

err = wh.Verify(rawBody, http.Header{
    "Webhook-Id":        []string{r.Header.Get("webhook-id")},
    "Webhook-Timestamp": []string{r.Header.Get("webhook-timestamp")},
    "Webhook-Signature": []string{r.Header.Get("webhook-signature")},
})

Idempotency

Use webhook-id (the value, not the body) as your application-level idempotency key. The same alert delivered twice (retry, redeliver, duplicate dispatch) will carry the same webhook-id, so your business logic (creating tickets, sending notifications, writing to your own DB) just needs to check “have I processed this id?” — exactly-once behaviour without any extra plumbing.

Important: do NOT dedup blindly on webhook-id alone at the infrastructure layer

A common pitfall: a queue/proxy in front of your handler does if (seenWebhookIds.has(headers['webhook-id'])) return; and silently drops legitimate retries. Our dispatcher reuses webhook-id across retries on purpose — that’s how application-level idempotency works — but it means an infrastructure-layer dedup keyed only on webhook-id will swallow a retry that your handler actually wanted to see (e.g. the first attempt timed out before your handler committed). If you need infrastructure-layer dedup (retry-storm protection, queue fan-out, observability counters), key on the tuple (webhook-id, webhook-attempt) instead. The webhook-attempt header is the delivery attempt number — 1 for the first delivery, 2 for the first retry, and so on.
// Good — infra-layer dedup that doesn't swallow retries
const dedupKey = `${headers['webhook-id']}:${headers['webhook-attempt']}`;
if (seenDeliveries.has(dedupKey)) return; // exact same attempt arrived twice
seenDeliveries.add(dedupKey);
// Application-layer idempotency — `webhook-id` is correct here
const alertId = headers['webhook-id'];
if (await alertsProcessed.has(alertId)) return; // already handled this alert
await processAlert(body);
await alertsProcessed.insert(alertId);
Security note: webhook-attempt is NOT part of the signed envelope. Per the Standard Webhooks spec, only webhook-id, webhook-timestamp, and the body are signed. An attacker who replays a captured request can set any webhook-attempt value they like. Use it only for dedup-counting and observability — never as input to a security decision.

Common pitfalls

  • Re-serialising the body. Verify against the bytes you received, not against JSON.stringify(JSON.parse(body)). Whitespace matters.
  • Comma vs space in webhook-signature. During rotation the header contains TWO entries separated by a SINGLE SPACE. Some libraries split on commas — make sure yours follows the spec.
  • Forgetting timestamp freshness. A leaked secret + stale signature is replayable. The SDK helper enforces 5-min skew automatically; if you roll your own, do the same.