Skip to content

Signature Verification

Every webhook request sent by Sangho contains a Sangho-Signature header. This header is an HMAC-SHA256 signature computed with your webhook secret over the raw body of the request.

Why verify the signature

  • Guarantee that the request genuinely comes from Sangho.
  • Protect against replay attacks.
  • Validate the integrity of the request body.
  • Prevent the processing of forged webhooks.

Header format

The Sangho-Signature header contains the HMAC-SHA256 signature of the body in hexadecimal, prefixed with sha256=. Two complementary headers are also sent:

HeaderDescription
Sangho-Signaturesha256=<hex> — HMAC signature of the raw body.
Sangho-Event-IDUnique identifier of the event — use it for deduplication.
Sangho-TimestampUnix timestamp of when it was sent — protects against replays.
Always use the raw body

The signature is computed over the raw body (raw bytes) of the request, before any JSON parsing. In Express.js, configure express.raw({ type: 'application/json' }) — never go through express.json(), which would transform the body before verification.

Best practices

  • Verify the signature before processing the event.
  • Return HTTP 200 immediately — process in the background.
  • Store the Sangho-Event-ID to deduplicate multiple deliveries.
  • Compare signatures using a method resistant to timing attacks (timingSafeEqual, hmac.compare_digest, hash_equals).

Headers of a webhook request

ResponseExample incoming request
Headers sent by Sangho to your endpoint on every event.
http
POST /webhook HTTP/1.1
Content-Type: application/json
Sangho-Signature: sha256=a2b3c4d5e6f7...
Sangho-Event-ID: evt_xxxxxxxxxxxx
Sangho-Timestamp: 1709294400

Manual verification (openssl)

bash
echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex
# Compare with the value in Sangho-Signature (without the sha256= prefix)

Implementation by language

Each example on the right illustrates the complete verification: reading the raw body, computing the HMAC-SHA256, a comparison resistant to timing attacks, then JSON parsing.

For the official SDKs, use the helper method directly: sangho.webhooks.constructEvent(body, signature, secret) in JavaScript.

Respond fast, process asynchronously

Your endpoint must return a 200 within 30 seconds. Queue the task in an asynchronous queue (Celery, Bull, Sidekiq…) and respond immediately. Past this delay, Sangho considers the delivery failed and triggers the retry policy.

Implementation by language

javascript
import express from 'express';
import crypto from 'crypto';


const app = express();


app.post(
  '/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig    = req.headers['sangho-signature'] as string;
    const secret = process.env.WEBHOOK_SECRET!;


    const expected = 'sha256=' + crypto
      .createHmac('sha256', secret)
      .update(req.body)
      .digest('hex');


    if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
      return res.status(400).send('Invalid signature');
    }


    const event = JSON.parse(req.body.toString());
    // Process in background, respond immediately
    processEvent(event).catch(console.error);
    res.json({ received: true });
  },
);