SARAH

Webhook Authentication

Complete guide to security and authentication for Sarah's webhooks

Last updated: 2025-01-26

Sarah implements multiple security layers to protect webhooks and ensure data integrity.

Security Layers

1. Bearer Token (Inbound)

For inbound webhooks (external systems sending events to Sarah):

Authorization: Bearer <integration_key>

The integration_key is unique per company and is obtained from Settings > Integrations.

Characteristics:

  • Required for all inbound requests
  • Uniquely identifies the company
  • Must be kept secret
  • Can be regenerated from settings

2. Internal Key (Outbound)

For outbound webhooks (Sarah sending events to external systems):

http
x-internal-key: <INTEGRATION_TRIGGER_KEY>
x-company-id: <numeric-company-id>

Only applies if the INTEGRATION_TRIGGER_KEY environment variable is configured.

Characteristics:

  • Optional (only if configured)
  • Protects internal endpoints
  • Must match exactly with environment variable

3. HMAC Signature

Cryptographic signature to verify payload integrity and authenticity.

For Inbound (Sending to Sarah)

If you send events to Sarah and want to sign them:

  1. Serialize JSON exactly (no extra spaces):

    const rawBody = JSON.stringify(envelope);
    
  2. Calculate HMAC SHA-256 using your integration_key as secret:

    javascript
    const crypto = require('crypto');
    const signature = crypto
      .createHmac('sha256', integrationKey)
      .update(rawBody)
      .digest('hex');
  3. Send in header:

    x-signature: <hex-signature>
    

Complete example:

javascript
const integrationKey = 'sk_live_abc123...';
const envelope = {
  event: 'product.upserted',
  data: { product: { id: 123, stock: 40 } }
};

const rawBody = JSON.stringify(envelope);
const signature = crypto
  .createHmac('sha256', integrationKey)
  .update(rawBody)
  .digest('hex');

fetch('https://sarah.ar/api/integration/in', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${integrationKey}`,
    'Content-Type': 'application/json',
    'x-signature': signature
  },
  body: rawBody
});

For Outbound (Receiving from Sarah)

If you receive events from Sarah and MAKER_WH_HMAC_SECRET is configured:

  1. Capture raw body before parsing:

    const rawBody = await request.text();
    
  2. Get signature from header:

    javascript
    const signature = request.headers.get('x-signature');
  3. Calculate expected HMAC:

    javascript
    const secret = process.env.MAKER_WH_HMAC_SECRET;
    const expected = crypto
      .createHmac('sha256', secret)
      .update(rawBody)
      .digest('hex');
  4. Compare securely (timing-safe):

    javascript
    if (signature !== expected) {
      return new Response('Invalid signature', { status: 401 });
    }

Complete example:

javascript
export async function POST(request) {
  // 1. Capture raw body
  const rawBody = await request.text();
  
  // 2. Get signature
  const signature = request.headers.get('x-signature');
  if (!signature) {
    return new Response('Missing signature', { status: 401 });
  }
  
  // 3. Validate HMAC
  const secret = process.env.MAKER_WH_HMAC_SECRET;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  
  // 4. Timing-safe comparison
  if (signature.toLowerCase() !== expected.toLowerCase()) {
    return new Response('Invalid signature', { status: 401 });
  }
  
  // 5. Parse and process
  const { event, data } = JSON.parse(rawBody);
  await processEvent(event, data);
  
  return new Response(JSON.stringify({ status: 'ok' }), {
    status: 200,
    headers: { 'Content-Type': 'application/json' }
  });
}

4. Replay Protection

Protection against replay attacks using timestamps.

For Inbound

Include x-sent-at header with an ISO timestamp:

x-sent-at: 2025-01-26T12:34:56.789Z

Sarah will reject events outside a ±5 minute window.

Example:

javascript
const sentAt = new Date().toISOString();

fetch('https://sarah.ar/api/integration/in', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${integrationKey}`,
    'Content-Type': 'application/json',
    'x-sent-at': sentAt
  },
  body: JSON.stringify(envelope)
});

For Outbound

Validate x-sent-at header when receiving events:

javascript
const sentAt = request.headers.get('x-sent-at');
if (sentAt) {
  const sentDate = new Date(sentAt);
  const ageMs = Date.now() - sentDate.getTime();
  
  // Reject if outside ±5 minutes
  if (Math.abs(ageMs) > 5 * 60 * 1000) {
    return new Response('x-sent-at outside 5m window', { status: 400 });
  }
}

5. Idempotency

Prevents duplicate processing using unique keys.

For Inbound

Include x-idempotency-key when sending events:

x-idempotency-key: product.upserted:123

Sarah will process the event only once. If you send the same event with the same key, you'll receive { "idempotent": true }.

Best practices for generating keys:

  • Use format: <event>:<unique-identifier>
  • Examples:
    • product.upserted:123
    • sales.created:789
    • movement.created:555:2025-01-26

For Outbound

Use x-idempotency-key to avoid processing the same event multiple times:

javascript
const idempotencyKey = request.headers.get('x-idempotency-key');

// Verify if already processed
if (await isProcessed(idempotencyKey)) {
  return new Response(JSON.stringify({ idempotent: true }), {
    status: 200,
    headers: { 'Content-Type': 'application/json' }
  });
}

// Process event
await processEvent(event, data);

// Mark as processed
await markProcessed(idempotencyKey);

Header Summary

Inbound (Sending to Sarah)

HeaderRequiredDescription
Authorization✅ YesBearer <integration_key>
Content-Type✅ Yesapplication/json
x-idempotency-key⚠️ RecommendedUnique key for idempotency
x-sent-at⚠️ RecommendedISO timestamp for replay protection
x-signature⚠️ OptionalHMAC SHA-256 hex signature

Outbound (Receiving from Sarah)

HeaderDescription
content-typeapplication/json
user-agentsarah-outbound-webhook/1.0
x-idempotency-keyUnique key for idempotency
x-sent-atISO timestamp of send
x-signatureHMAC SHA-256 hex (if configured)

Environment Variable Configuration

For outbound webhooks, configure these variables:

bash
# External webhook URL
MAKER_WH_URL=https://your-webhook.com/events

# Secret to sign events (optional but recommended)
MAKER_WH_HMAC_SECRET=your-secret-key-here

# Internal key to authenticate requests (optional)
INTEGRATION_TRIGGER_KEY=internal-secret-key

Security Best Practices

  1. Always use HTTPS - Never send webhooks over HTTP
  2. Sign with HMAC - Enable MAKER_WH_HMAC_SECRET for outbound
  3. Validate timestamps - Implement replay protection
  4. Use idempotency keys - Prevents duplicate processing
  5. Rotate secrets periodically - Regenerate integration_key and MAKER_WH_HMAC_SECRET
  6. Log failed attempts - Monitor invalid authentication attempts
  7. Limit rate limiting - Implement throttling in your webhook handler
  8. Keep secrets secure - Never expose them in code or logs

Troubleshooting

Error: "Missing bearer"

  • Verify Authorization header is present
  • Ensure you use format Bearer <key>

Error: "Invalid integration key"

  • Verify integration_key is correct
  • Regenerate key from Settings > Integrations if necessary

Error: "Invalid signature"

  • Verify you're using correct secret
  • Ensure you calculate HMAC on raw body (before parsing)
  • Verify you're not adding extra spaces to JSON

Error: "x-sent-at outside 5m window"

  • Verify your server clock is synchronized
  • Ensure you send timestamp immediately before request