Webhooks

Webhooks deliver signed outbound POST requests for selected tenant events, so you can react to changes without polling. Manage endpoints from Settings -> Integrations -> Webhooks or the public API.

Events

EventStatusDelivery
inventory.item.createdEmittedSigned outbox delivery
inventory.item.updatedEmittedSigned outbox delivery
order.createdEmittedSigned outbox delivery
order.updatedEmittedSigned outbox delivery
contact.createdEmittedSigned outbox delivery
contact.updatedEmittedSigned outbox delivery
stock_document.createdEmittedSigned outbox delivery
stock_document.updatedEmittedSigned outbox delivery
user.invitedEmittedSigned outbox delivery
organization.updatedEmittedSigned outbox delivery
integration.connectedEmittedSigned outbox delivery
bom.createdPlannedContract reserved, not emitted yet
bom.updatedPlannedContract reserved, not emitted yet
webhook.testTest onlyUnsigned connectivity check

Subscribe

Create an endpoint with POST /v1/webhooks (scope webhooks:write, Enterprise). The signing secret is returned once in the response - store it immediately.

curl -X POST https://api.fabhub.app/v1/webhooks \
  -H "X-API-Key: $FABHUB_API_KEY" \
  -H "Idempotency-Key: 7c1f...-..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Orders sync","targetUrl":"https://example.com/hooks/fabhub","subscribedEvents":["order.created","order.updated"]}'
{
  "data": {
    "id": "wh_1",
    "name": "Orders sync",
    "targetUrl": "https://example.com/hooks/fabhub",
    "status": "active",
    "environment": "production",
    "description": null,
    "subscribedEvents": ["order.created", "order.updated"],
    "createdAt": "2026-06-20T09:00:00Z",
    "updatedAt": "2026-06-20T09:00:00Z"
  },
  "signingSecret": "whsec_9f3a...stored-once"
}

Delivery format

Each delivery is a POST with the JSON event body and these headers:

  • X-FabHub-Event - the event type, for example order.created
  • X-FabHub-Timestamp - unix seconds when the payload was signed
  • X-FabHub-Signature - v1=<hex> HMAC; multiple comma-separated v1= parts appear during secret rotation
POST /hooks/fabhub HTTP/1.1
X-FabHub-Event: order.created
X-FabHub-Timestamp: 1718873400
X-FabHub-Signature: v1=4f2c...e1

{ "event": "order.created", "data": { "id": "ord_1", "module": "sell", "status": "open" } }

Verify the signature

The signature is HMAC-SHA256(secret, "<timestamp>.<rawBody>"), hex-encoded, where secret is your signing secret decoded from hex. Always verify against the exact raw request body, before JSON parsing. The SDK ships a verifier:

import { verifyFabHubWebhookSignature } from '@fabhub/sdk';

const result = verifyFabHubWebhookSignature({
  signingSecret: process.env.FABHUB_WEBHOOK_SECRET,
  rawBody,
  timestamp: req.headers['x-fabhub-timestamp'],
  signature: req.headers['x-fabhub-signature'],
  // toleranceSeconds: 300 (default) - rejects stale/replayed timestamps
});

if (!result.ok) return res.status(400).end();
// safe to JSON.parse(rawBody) now

Secret rotation

Rotate with PATCH /v1/webhooks/{webhook_id} and {"rotateSecret": true}. The new secret is returned once, and during the overlap window deliveries are signed with both the new and previous secrets (pass both to the verifier via signingSecrets).


Delivery logs

Inspect attempts with GET /v1/webhooks/{webhook_id}/deliveries (scope webhooks:deliveries:read):

{
  "data": [
    {
      "id": "del_1",
      "eventType": "order.created",
      "status": "delivered",
      "attempts": 1,
      "lastError": null,
      "lastHttpStatus": 200,
      "createdAt": "2026-06-20T09:01:00Z",
      "updatedAt": "2026-06-20T09:01:01Z"
    }
  ],
  "pagination": { "page": 1, "pageSize": 20, "total": 1, "totalPages": 1 }
}

Synthetic test pings (webhook.test) are unsigned connectivity checks and do not appear in the outbox log.


Best practices

  • Return 2xx quickly; do heavy work asynchronously.
  • Treat delivery as at-least-once and dedupe on the event identity.
  • Filter on X-FabHub-Event and ignore event types you do not handle.