> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sakawave.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify webhook signatures

> Verify Sakawave signatures against the exact raw request body in Node.js and Python.

Endpoint creation and explicit secret rotation disclose a `whsec_…` secret once. Store it in a secret manager. Editing the endpoint URL does not rotate the secret.

Sakawave signs this byte sequence:

```text theme={null}
<event-id>.<unix-timestamp>.<raw-body>
```

Compare the supplied `v1=<hex>` signature with an HMAC-SHA-256 digest using a constant-time comparison. Reject missing headers, malformed signatures, and timestamps outside your tolerance before parsing JSON.

## Node.js with Express

```ts theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

const app = express();
const secret = process.env.SAKAWAVE_WEBHOOK_SECRET;
if (!secret) throw new Error('SAKAWAVE_WEBHOOK_SECRET is required');

app.post('/webhooks/sakawave', express.raw({ type: 'application/json' }), async (req, res) => {
  const eventId = req.get('Sakawave-Webhook-Id');
  const timestamp = req.get('Sakawave-Webhook-Timestamp');
  const signature = req.get('Sakawave-Webhook-Signature');

  if (!eventId || !timestamp || !signature?.startsWith('v1=')) {
    return res.status(400).send('Invalid webhook headers');
  }

  const timestampNumber = Number(timestamp);
  const ageSeconds = Math.abs(Date.now() / 1000 - timestampNumber);
  if (!Number.isFinite(timestampNumber) || ageSeconds > 300) {
    return res.status(400).send('Webhook timestamp outside tolerance');
  }

  const expected = createHmac('sha256', secret)
    .update(eventId)
    .update('.')
    .update(timestamp)
    .update('.')
    .update(req.body)
    .digest('hex');
  const received = signature.slice(3);
  const expectedBytes = Buffer.from(expected, 'hex');
  const receivedBytes = Buffer.from(received, 'hex');
  const valid =
    expectedBytes.length === receivedBytes.length &&
    timingSafeEqual(expectedBytes, receivedBytes);

  if (!valid) return res.status(401).send('Invalid signature');

  // The event ID is a header. The body has only `event` and `data`.
  const event = JSON.parse(req.body.toString('utf8'));
  await enqueueOnce(eventId, event);
  return res.sendStatus(204);
});
```

Register the raw-body route before any global `express.json()` middleware. `enqueueOnce` must use a durable unique constraint on the webhook ID from the `Sakawave-Webhook-Id` header.

## Python with Flask

```python theme={null}
import hashlib
import hmac
import json
import os
import time
from flask import Flask, request

app = Flask(__name__)
secret = os.environ["SAKAWAVE_WEBHOOK_SECRET"].encode("utf-8")

@app.post("/webhooks/sakawave")
def sakawave_webhook():
    event_id = request.headers.get("Sakawave-Webhook-Id")
    timestamp = request.headers.get("Sakawave-Webhook-Timestamp")
    supplied = request.headers.get("Sakawave-Webhook-Signature")
    if not event_id or not timestamp or not supplied or not supplied.startswith("v1="):
        return "Invalid webhook headers", 400

    try:
        timestamp_number = int(timestamp)
    except ValueError:
        return "Invalid webhook timestamp", 400
    if abs(time.time() - timestamp_number) > 300:
        return "Webhook timestamp outside tolerance", 400

    raw_body = request.get_data(cache=True)
    signed = event_id.encode() + b"." + timestamp.encode() + b"." + raw_body
    expected = hmac.new(secret, signed, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, supplied[3:]):
        return "Invalid signature", 401

    # The event ID is a header. The body has only `event` and `data`.
    event = json.loads(raw_body)
    enqueue_once(event_id, event)
    return "", 204
```

Never parse and re-serialize JSON before verification. Whitespace and property order are part of the signed bytes.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.