SARAH

Webhooks Inbound

Guide to receive events from external systems in Sarah

Last updated: 2025-01-26

The /api/integration/in endpoint allows external systems (such as Make or Zapier) to send events to Sarah to create or update domain entities or notify status changes.

Endpoint

POST /api/integration/in
GET /api/integration/in

Authentication

All requests must include a Bearer authentication header:

Authorization: Bearer <integration_key>

The integration_key must match a company record in Sarah. You can obtain your Integration Key from Settings > Integrations.

POST /api/integration/in

Sends an event to Sarah using a consistent envelope.

http
Authorization: Bearer <integration_key>
Content-Type: application/json
x-idempotency-key: <unique-string>
x-sent-at: 2025-01-26T12:34:56.789Z
x-signature: <optional hmac hex>

Envelope Structure

All events must follow this structure:

json
{
  "event": "product.upserted",
  "data": {
    /* event-specific payload object */
  }
}

Supported Events

EventDescriptiondata Structure
sales.createdNew sale created{ "sale": { ... } }
sales.paidSale marked as paid{ "sale": { "id": 123 } }
sales.cancelledSale cancelled{ "sale": { "id": 123 } }
invoice.issuedFiscal receipt issued{ "invoice": { ... } }
movement.createdInventory movement created{ "movement": { ... } }
product.upsertedProduct created or updated{ "product": { ... } }
product.stock_updatedProduct stock updated{ "product": { "id": 123, "stock": 50 } }

Example: Update Product

http
POST /api/integration/in
Authorization: Bearer sk_live_abc123...
Content-Type: application/json
x-idempotency-key: product.upserted:123
x-sent-at: 2025-01-26T12:34:56.789Z

{
  "event": "product.upserted",
  "data": {
    "product": {
      "id": 123,
      "sku": "ABC-001",
      "name": "Example Product",
      "stock": 40,
      "base_price": 1500.00
    }
  }
}

Responses

Success (200):

json
{
  "status": "ok"
}

Success with idempotency (200):

json
{
  "status": "ok",
  "idempotent": true
}

Validation error (4xx):

json
{
  "error": "Invalid or missing event"
}

Security

For enhanced security, you can include an HMAC signature:

  1. Serialize the JSON exactly (without extra spaces)
  2. Calculate HMAC SHA-256 with your integration_key as secret
  3. Convert to lowercase hexadecimal string
  4. Send in header: x-signature: <hex>

If the signature is present but doesn't match, the request will be rejected with 401.

Replay Protection

The x-sent-at header must contain an ISO timestamp. Sarah will reject events outside a ±5 minutes window.

Idempotency

The x-idempotency-key header ensures an event is only processed once. If you send the same event with the same key, you'll receive { "idempotent": true } without side effects.

GET /api/integration/in

Query stored events for your company (polling).

Headers

Authorization: Bearer <integration_key>

Query Parameters

ParameterDescriptionDefault
limitMaximum events to return50 (max 200)
sinceISO timestamp; returns events with received_at > sincenull

Example

http
GET /api/integration/in?limit=10&since=2025-01-26T10:00:00Z
Authorization: Bearer sk_live_abc123...

Response

json
{
  "events": [
    {
      "id": "uuid",
      "received_at": "2025-01-26T12:35:15.000Z",
      "event": "product.upserted",
      "idempotency_key": "product.upserted:123",
      "payload": {
        "event": "product.upserted",
        "data": { ... }
      },
      "signature_valid": true
    }
  ],
  "meta": {
    "limit": 50,
    "since": null
  }
}

Status Codes

CodeDescription
200Success
400Invalid JSON or x-sent-at outside window
401Missing, invalid Bearer, or incorrect HMAC signature
422Invalid event or incorrect data structure
500Internal server error

Best Practices

  1. Always use idempotency keys when you can retry (e.g., network uncertainty)
  2. Provide x-sent-at and x-signature for enhanced security
  3. Use incremental polling via GET /api/integration/in?since=<last_received_at> if webhooks are not feasible
  4. Log the idempotent flag to suppress duplicate downstream processing
  5. Handle 4xx errors appropriately and don't retry indefinitely