Webhooks and Integrations
Complete guide to integrate Sarah with automation platforms like Make or Zapier
Last updated: 2025-01-26
Webhooks and Integrations (Inbound & Outbound)
This documentation summarizes how to consume (inbound) and receive (outbound) integration webhooks for automation platforms like Make or Zapier.
Summary
Two symmetric HTTP endpoints:
| Direction | Path | Methods | Purpose |
|---|---|---|---|
| Inbound | /api/integration/in | POST, GET | External systems send events TO Sarah (create/update entities or notify state changes). GET provides polling access to previously stored events. |
| Outbound | /api/integration/out | POST, GET | Internal system sends events FROM Sarah TO an external automation webhook. GET provides introspection metadata. |
All event payloads use a consistent envelope form:
json{
"event": "product.upserted",
"data": { /* event-specific payload object */ }
}
Supported Events (current enum)
sales.created
sales.paid
sales.cancelled
invoice.issued
movement.created
product.upserted
product.stock_updated
Extensible in the future—unrecognized events are ignored (safe no-op) internally.
Authentication and Security Layers
| Mechanism | Applies To | Description |
|---|---|---|
| Bearer Token | Inbound (POST/GET) | Authorization: Bearer <integration_key> where integration_key matches a company record. Required. |
| Internal Key | Outbound (POST) | If environment variable INTEGRATION_TRIGGER_KEY is configured, request must include header x-internal-key: <value>. |
| HMAC Signature | Inbound (optional verification), Outbound (optional production) | Header x-signature containing hex(HMAC_SHA256(secret, raw_body)). Inbound secret = integration key. Outbound secret = MAKER_WH_HMAC_SECRET (if present). |
| Replay Protection | Inbound & Outbound | Header x-sent-at (ISO timestamp). Inbound rejects if outside ±5 minute window. |
| Idempotency | Both directions | Header x-idempotency-key ensures a payload is processed/stored only once per company. Duplicate inbound returns { idempotent: true }. |
Generating HMAC (Outbound or External Sender)
Pseudo steps:
- Serialize JSON exactly (no extra spaces) — implementation uses
JSON.stringify(envelope). - Calculate HMAC SHA-256 with UTF‑8 secret over raw bytes.
- Convert resulting bytes to lowercase hex string.
- Send in header:
x-signature: <hex>.
If signature is present inbound but verification fails → 401.
Inbound Endpoint Details
POST /api/integration/in
External automation publishes an envelope.
Headers (recommended):
httpAuthorization: Bearer <integration_key>
Content-Type: application/json
x-idempotency-key: <unique-string>
x-sent-at: 2025-09-02T12:34:56.789Z
x-signature: <optional hmac hex>
Example body (product update):
json{
"event": "product.upserted",
"data": {
"product": { "id": 123, "sku": "ABC-001", "stock": 40 }
}
}
Successful responses:
{ "status": "ok" }
Duplicate send (idempotent):
{ "status": "ok", "idempotent": true }
Validation failures return 4xx with { error: "..." }.
Processing Stages:
- Auth & raw body capture
- Replay window verification (
x-sent-at± 5m if header present) - Event form & payload validation (minimum schema per event type)
- Optional HMAC verification (only if header present)
- Idempotency check
- Persistence in
integration_events - Lightweight domain verification handler (non-blocking for success state)
GET /api/integration/in
Poll stored events for a company.
Headers:
Authorization: Bearer <integration_key>
Query params:
| Param | Description |
|---|---|
limit | Max events (default 50, max 200) |
since | ISO timestamp; returns events with received_at > since |
Response structure:
json{
"events": [
{
"id": "uuid",
"received_at": "2025-09-02T12:35:15.000Z",
"event": "product.upserted",
"idempotency_key": "product.upserted:123",
"payload": { "event": "product.upserted", "data": { ... } },
"signature_valid": true
}
],
"meta": { "limit": 50, "since": null }
}
Outbound Endpoint Details
POST /api/integration/out
Triggers a send to the configured automation webhook (MAKER_WH_URL).
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:
- Direct data:
json{
"event": "movement.created",
"data": { "movement": { "id": 555, "type": "IN", "amount": 10 } },
"idempotency_key": "movement.created:555"
}
- Reference expansion (system builds data via Supabase lookups):
json{
"event": "product.upserted",
"refs": { "product_id": 123 },
"idempotency_key": "product.upserted:123"
}
If both data and refs are provided, data wins; refs is used only when data is absent.
Outbound headers generated to target:
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 (and not disabled)
Example response:
json{
"sent": true,
"target": { "status": 200, "body": { "received": true } },
"envelope": { "event": "product.upserted", "data": { "product": { "id": 123 } } },
"idempotency_key": "product.upserted:123",
"used_hmac": true
}
GET /api/integration/out
Returns capability metadata:
json{
"outbound": true,
"supported_events": ["product.upserted", ...],
"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" }
}
Error Handling Summary
| Condition | Status | Example Body |
|---|---|---|
| Missing bearer (inbound) | 401 | { "error": "Missing bearer" } |
| Invalid JSON | 400 | { "error": "Invalid JSON" } |
| Invalid event | 422 | { "error": "Invalid or missing event" } |
| Replay window failure | 400 | { "error": "x-sent-at outside 5m window" } |
| HMAC mismatch | 401 | { "error": "Invalid signature" } |
| Duplicate (idempotent) | 200 | { "status": "ok", "idempotent": true } |
| Missing outbound target | 500 | { "error": "MAKER_WH_URL not configured" } |
| Outbound dispatch failure | 502 | { "error": "Dispatch failed", "detail": "..." } |
Implementation Notes
| Concern | Approach |
|---|---|
| Runtime | Next.js Edge (native Web Crypto) |
| Storage | Supabase integration_events table for inbound persistence |
| Signature comparison | Constant time (length & XOR fold) for hex strings |
| Handler failures | Logged; don't impact 200 response (prevents noisy retries) |
| Extensibility | Add new events to enum + handler map; outbound auto-supports |
Best Practices for Integrators
- Always send an idempotency key when you can retry (e.g., network uncertainty).
- Provide
x-sent-at&x-signaturefor elevated security (prevents tampering & replay). - Use incremental polling via
GET /api/integration/in?since=<last_received_at>if webhooks aren't feasible on your side. - Log the returned
idempotentflag to suppress duplicate processing downstream. - For outbound consumption (your platform receiving events from Sarah), validate signature early and reject events >5m old based on
x-sent-at.
Changelog (initial)
| Date | Change |
|---|---|
| 2025-09-02 | Initial documentation + endpoints implemented |
Questions or new event requirements: extend enum & handlers; keep envelope stable for forward compatibility.