Skip to main content

Webhook Signatures

Every webhook request includes a signature in the X-Sturdy-Signature header. This signature allows you to verify that the webhook was sent by Sturdy Technologies and hasn’t been tampered with.

How Signatures Work

We generate signatures using HMAC-SHA256 with your webhook secret key. The signature is calculated directly from the raw request body.

Verification Steps

1. Extract the Signature

Get the signature from the request header:
const signature = request.headers['x-sturdy-signature'];

2. Calculate Expected Signature

const crypto = require('crypto');

const expectedSignature = crypto
  .createHmac('sha256', webhookSecret)
  .update(requestBody)
  .digest('hex');

3. Compare Signatures

const isValid = crypto.timingSafeEqual(
  Buffer.from(signature),
  Buffer.from(expectedSignature)
);

if (!isValid) {
  throw new Error('Invalid signature');
}

Complete Example (Node.js)

const express = require('express');
const crypto = require('crypto');

const app = express();

// Raw body needed for signature verification
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-sturdy-signature'];
  const webhookSecret = process.env.WEBHOOK_SECRET;

  // Verify signature
  const expectedSignature = crypto
    .createHmac('sha256', webhookSecret)
    .update(req.body)
    .digest('hex');

  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );

  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

  // Process webhook
  const event = JSON.parse(req.body);
  console.log('Received event:', event.event_type);

  res.status(200).send('OK');
});

Preventing Replay Attacks

Use the event_id fields in the webhook payload to prevent replay attacks:
const event = JSON.parse(req.body);

// Check if event was already processed
if (await isEventProcessed(event.event_id)) {
  return res.status(200).send('Already processed');
}

// Mark event as processed
await markEventAsProcessed(event.event_id);

IP Allowlisting

For additional security, you can allowlist our webhook IP addresses: 203.0.113.10 203.0.113.11 203.0.113.12
Contact support for the current list of webhook IP addresses for production.

Additional Examples

import hmac
import hashlib

def verify_webhook(request, webhook_secret):
signature = request.headers.get('X-Sturdy-Signature')

    # Calculate expected signature
    expected_signature = hmac.new(
        webhook_secret.encode('utf-8'),
        request.body,
        hashlib.sha256
    ).hexdigest()

    # Compare signatures
    if not hmac.compare_digest(signature, expected_signature):
        raise ValueError('Invalid signature')

    return True

Security Checklist

1

Verify Signatures

Always verify the X-Sturdy-Signature header on every request
2

Use HTTPS

Only accept webhooks on HTTPS endpoints
3

Keep Secrets Safe

Store your webhook secret securely (environment variables, secret manager)
4

Implement Idempotency

Track processed event_id values to prevent duplicate processing