Webhooks Outbound
Guide to receive events from Sarah in external systems
Last updated: 2025-01-26
The /api/integration/out endpoint allows Sarah's internal system to send events to a configured external webhook (for example, Make or Zapier) to automate processes.
Endpoint
POST /api/integration/out
GET /api/integration/out
Configuration
To use outbound webhooks, you need to configure the following environment variables:
MAKER_WH_URL: URL of external webhook that will receive eventsMAKER_WH_HMAC_SECRET(optional): Secret to sign events with HMACINTEGRATION_TRIGGER_KEY(optional): Internal key to authenticate requests
POST /api/integration/out
Triggers a send to the configured automation webhook.
Authentication
If INTEGRATION_TRIGGER_KEY is configured, you must include:
httpx-internal-key: <INTEGRATION_TRIGGER_KEY>
x-company-id: <numeric-company-id>
Headers
httpContent-Type: application/json
x-company-id: <numeric-company-id>
x-internal-key: <internal secret> # only if INTEGRATION_TRIGGER_KEY is configured
Body Options
1. Direct Data
Send complete payload directly:
json{
"event": "movement.created",
"data": {
"movement": {
"id": 555,
"type": "IN",
"amount": 10,
"product_id": 123
}
},
"idempotency_key": "movement.created:555"
}
2. Reference Expansion
System builds data automatically through Supabase lookups:
json{
"event": "product.upserted",
"refs": {
"product_id": 123
},
"idempotency_key": "product.upserted:123"
}
If both data and refs are provided, data takes priority.
Generated Headers
Sarah automatically generates the following headers when sending to external webhook:
httpcontent-type: application/json
user-agent: sarah-outbound-webhook/1.0
x-idempotency-key: <provided-or-generated>
x-sent-at: <ISO timestamp>
x-signature: <HMAC> # only if MAKER_WH_HMAC_SECRET is configured
Request Example
httpPOST /api/integration/out
Content-Type: application/json
x-company-id: 42
x-internal-key: secret-key-123
{
"event": "sales.created",
"refs": {
"sale_id": 789
},
"idempotency_key": "sales.created:789"
}
Response
json{
"sent": true,
"target": {
"status": 200,
"body": {
"received": true
}
},
"envelope": {
"event": "sales.created",
"data": {
"sale": {
"id": 789,
"total": 15000.00,
"status": "pending"
}
}
},
"idempotency_key": "sales.created:789",
"used_hmac": true
}
GET /api/integration/out
Returns metadata of capabilities and configuration.
Example
GET /api/integration/out
Response
json{
"outbound": true,
"supported_events": [
"sales.created",
"sales.paid",
"sales.cancelled",
"invoice.issued",
"movement.created",
"product.upserted",
"product.stock_updated"
],
"requires_internal_key": true,
"hmac_enabled": true,
"env_target_set": true,
"example_body": {
"event": "product.upserted",
"refs": {
"product_id": 123
},
"idempotency_key": "product.upserted:123"
}
}
Receiving Outbound Webhooks
If you're configuring an external webhook (Make, Zapier, etc.) to receive events from Sarah:
Envelope Structure
Events follow the same structure as inbound:
json{
"event": "sales.created",
"data": {
"sale": {
"id": 789,
"total": 15000.00,
"status": "pending",
"items": [...]
}
}
}
HMAC Signature Validation
If MAKER_WH_HMAC_SECRET is configured, Sarah will include an x-signature header:
- Capture raw body of request
- Calculate
HMAC_SHA256(MAKER_WH_HMAC_SECRET, raw_body) - Convert to lowercase hexadecimal
- Compare with
x-signatureheader
If they don't match, reject the request.
Replay Protection
Validate x-sent-at header:
- Must be a valid ISO timestamp
- Must not be older than 5 minutes
- Reject events outside this window
Idempotency
Use x-idempotency-key header to avoid processing the same event multiple times:
- Store processed keys
- If you receive a duplicate key, return 200 without side effects
- Clean old keys periodically
Handler Example (Node.js)
javascriptimport crypto from 'crypto';
export async function POST(request) {
const rawBody = await request.text();
const body = JSON.parse(rawBody);
const signature = request.headers.get('x-signature');
const sentAt = request.headers.get('x-sent-at');
const idempotencyKey = request.headers.get('x-idempotency-key');
// Validate timestamp
const sentDate = new Date(sentAt);
const ageMs = Date.now() - sentDate.getTime();
if (Math.abs(ageMs) > 5 * 60 * 1000) {
return new Response('x-sent-at outside 5m window', { status: 400 });
}
// Validate HMAC
const secret = process.env.MAKER_WH_HMAC_SECRET;
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
if (signature !== expected) {
return new Response('Invalid signature', { status: 401 });
}
// Verify idempotency
if (await isProcessed(idempotencyKey)) {
return new Response(JSON.stringify({ idempotent: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
// Process event
await processEvent(body.event, body.data);
await markProcessed(idempotencyKey);
return new Response(JSON.stringify({ status: 'ok' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
Status Codes
| Code | Description |
|---|---|
| 200 | Success |
| 400 | Missing required fields |
| 401 | Invalid or missing x-internal-key |
| 404 | Company or referenced entity not found |
| 500 | MAKER_WH_URL not configured |
| 502 | Failed to send to external webhook |
Best Practices
- Configure HMAC to protect event integrity
- Validate
x-sent-atto prevent replay attacks - Use idempotency keys to avoid duplicate processing
- Handle errors gracefully - return 200 even if processing fails internally
- Log all events received for debugging and auditing