SARAH

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 events
  • MAKER_WH_HMAC_SECRET (optional): Secret to sign events with HMAC
  • INTEGRATION_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:

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

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

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:

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

Request Example

http
POST /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:

  1. Capture raw body of request
  2. Calculate HMAC_SHA256(MAKER_WH_HMAC_SECRET, raw_body)
  3. Convert to lowercase hexadecimal
  4. Compare with x-signature header

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:

  1. Store processed keys
  2. If you receive a duplicate key, return 200 without side effects
  3. Clean old keys periodically

Handler Example (Node.js)

javascript
import 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

CodeDescription
200Success
400Missing required fields
401Invalid or missing x-internal-key
404Company or referenced entity not found
500MAKER_WH_URL not configured
502Failed to send to external webhook

Best Practices

  1. Configure HMAC to protect event integrity
  2. Validate x-sent-at to prevent replay attacks
  3. Use idempotency keys to avoid duplicate processing
  4. Handle errors gracefully - return 200 even if processing fails internally
  5. Log all events received for debugging and auditing