Skip to main content
Developers
5 min read

Webhooks

Get notified the moment a work order changes — signed, and retried until delivered.

Instead of polling, subscribe to events and we’ll POST them to your endpoint as they happen. Create a subscription with the webhooks:manage scope:

curl -X POST "https://app.facilityopsiq.com/api/v1/webhooks" \
  -H "Authorization: Bearer wo_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Prod sync",
    "url": "https://example.com/hooks/facilityopsiq",
    "events": ["record.created", "record.status_changed", "record.completed"]
  }'

The response includes a secret once — store it to verify signatures.

Events

record.created · record.status_changed · record.assigned · record.completed · record.closed · record.proof_delivered (a verifiable proof is now available) · asset.updated.

Delivery format

Each delivery is a POST with an X-Webhook-Event header and this body:

{
  "event": "record.status_changed",
  "payload": { "recordId": "WO-000188", "from": "ASSIGNED", "to": "IN_PROGRESS" },
  "timestamp": "2026-07-27T12:00:00.000Z"
}

Verifying signatures

We sign the raw request body with your subscription secret (HMAC-SHA256) and send it as X-Webhook-Signature: sha256=. Recompute it over the raw body and compare with a constant-time check before trusting the payload:

import { createHmac, timingSafeEqual } from 'crypto'

function verify(rawBody, header, secret) {
  const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex')
  const a = Buffer.from(header || ''), b = Buffer.from(expected)
  return a.length === b.length && timingSafeEqual(a, b)
}

Retries

If your endpoint returns a non-2xx or times out (10s), we retry with exponential backoff — up to 4 attempts — on network errors, 429, and 5xx. A 4xx (other than 429) is treated as a permanent rejection and not retried. Respond 2xx quickly and process asynchronously; make your handler idempotent, since a delivery can arrive more than once.