SARAH

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:

DirectionPathMethodsPurpose
Inbound/api/integration/inPOST, GETExternal systems send events TO Sarah (create/update entities or notify state changes). GET provides polling access to previously stored events.
Outbound/api/integration/outPOST, GETInternal 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

MechanismApplies ToDescription
Bearer TokenInbound (POST/GET)Authorization: Bearer <integration_key> where integration_key matches a company record. Required.
Internal KeyOutbound (POST)If environment variable INTEGRATION_TRIGGER_KEY is configured, request must include header x-internal-key: <value>.
HMAC SignatureInbound (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 ProtectionInbound & OutboundHeader x-sent-at (ISO timestamp). Inbound rejects if outside ±5 minute window.
IdempotencyBoth directionsHeader 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:

  1. Serialize JSON exactly (no extra spaces) — implementation uses JSON.stringify(envelope).
  2. Calculate HMAC SHA-256 with UTF‑8 secret over raw bytes.
  3. Convert resulting bytes to lowercase hex string.
  4. 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):

http
Authorization: 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:

  1. Auth & raw body capture
  2. Replay window verification (x-sent-at ± 5m if header present)
  3. Event form & payload validation (minimum schema per event type)
  4. Optional HMAC verification (only if header present)
  5. Idempotency check
  6. Persistence in integration_events
  7. 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:

ParamDescription
limitMax events (default 50, max 200)
sinceISO 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:

http
Content-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:
json
{
  "event": "movement.created",
  "data": { "movement": { "id": 555, "type": "IN", "amount": 10 } },
  "idempotency_key": "movement.created:555"
}
  1. 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:

http
content-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

ConditionStatusExample Body
Missing bearer (inbound)401{ "error": "Missing bearer" }
Invalid JSON400{ "error": "Invalid JSON" }
Invalid event422{ "error": "Invalid or missing event" }
Replay window failure400{ "error": "x-sent-at outside 5m window" }
HMAC mismatch401{ "error": "Invalid signature" }
Duplicate (idempotent)200{ "status": "ok", "idempotent": true }
Missing outbound target500{ "error": "MAKER_WH_URL not configured" }
Outbound dispatch failure502{ "error": "Dispatch failed", "detail": "..." }

Implementation Notes

ConcernApproach
RuntimeNext.js Edge (native Web Crypto)
StorageSupabase integration_events table for inbound persistence
Signature comparisonConstant time (length & XOR fold) for hex strings
Handler failuresLogged; don't impact 200 response (prevents noisy retries)
ExtensibilityAdd new events to enum + handler map; outbound auto-supports

Best Practices for Integrators

  1. Always send an idempotency key when you can retry (e.g., network uncertainty).
  2. Provide x-sent-at & x-signature for elevated security (prevents tampering & replay).
  3. Use incremental polling via GET /api/integration/in?since=<last_received_at> if webhooks aren't feasible on your side.
  4. Log the returned idempotent flag to suppress duplicate processing downstream.
  5. For outbound consumption (your platform receiving events from Sarah), validate signature early and reject events >5m old based on x-sent-at.

Changelog (initial)

DateChange
2025-09-02Initial documentation + endpoints implemented

Questions or new event requirements: extend enum & handlers; keep envelope stable for forward compatibility.