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):
httpx-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:
-
Serialize JSON exactly (no extra spaces):
const rawBody = JSON.stringify(envelope); -
Calculate HMAC SHA-256 using your
integration_keyas secret:javascriptconst crypto = require('crypto'); const signature = crypto .createHmac('sha256', integrationKey) .update(rawBody) .digest('hex'); -
Send in header:
x-signature: <hex-signature>
Complete example:
javascriptconst 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:
-
Capture raw body before parsing:
const rawBody = await request.text(); -
Get signature from header:
javascriptconst signature = request.headers.get('x-signature'); -
Calculate expected HMAC:
javascriptconst secret = process.env.MAKER_WH_HMAC_SECRET; const expected = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); -
Compare securely (timing-safe):
javascriptif (signature !== expected) { return new Response('Invalid signature', { status: 401 }); }
Complete example:
javascriptexport 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:
javascriptconst 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:
javascriptconst 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:123sales.created:789movement.created:555:2025-01-26
For Outbound
Use x-idempotency-key to avoid processing the same event multiple times:
javascriptconst 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)
| Header | Required | Description |
|---|---|---|
Authorization | ✅ Yes | Bearer <integration_key> |
Content-Type | ✅ Yes | application/json |
x-idempotency-key | ⚠️ Recommended | Unique key for idempotency |
x-sent-at | ⚠️ Recommended | ISO timestamp for replay protection |
x-signature | ⚠️ Optional | HMAC SHA-256 hex signature |
Outbound (Receiving from Sarah)
| Header | Description |
|---|---|
content-type | application/json |
user-agent | sarah-outbound-webhook/1.0 |
x-idempotency-key | Unique key for idempotency |
x-sent-at | ISO timestamp of send |
x-signature | HMAC 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
- Always use HTTPS - Never send webhooks over HTTP
- Sign with HMAC - Enable
MAKER_WH_HMAC_SECRETfor outbound - Validate timestamps - Implement replay protection
- Use idempotency keys - Prevents duplicate processing
- Rotate secrets periodically - Regenerate
integration_keyandMAKER_WH_HMAC_SECRET - Log failed attempts - Monitor invalid authentication attempts
- Limit rate limiting - Implement throttling in your webhook handler
- Keep secrets secure - Never expose them in code or logs
Troubleshooting
Error: "Missing bearer"
- Verify
Authorizationheader is present - Ensure you use format
Bearer <key>
Error: "Invalid integration key"
- Verify
integration_keyis 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