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
| Event | Status | Delivery |
|---|---|---|
inventory.item.created | Emitted | Signed outbox delivery |
inventory.item.updated | Emitted | Signed outbox delivery |
order.created | Emitted | Signed outbox delivery |
order.updated | Emitted | Signed outbox delivery |
contact.created | Emitted | Signed outbox delivery |
contact.updated | Emitted | Signed outbox delivery |
stock_document.created | Emitted | Signed outbox delivery |
stock_document.updated | Emitted | Signed outbox delivery |
user.invited | Emitted | Signed outbox delivery |
organization.updated | Emitted | Signed outbox delivery |
integration.connected | Emitted | Signed outbox delivery |
bom.created | Planned | Contract reserved, not emitted yet |
bom.updated | Planned | Contract reserved, not emitted yet |
webhook.test | Test only | Unsigned 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 exampleorder.createdX-FabHub-Timestamp- unix seconds when the payload was signedX-FabHub-Signature-v1=<hex>HMAC; multiple comma-separatedv1=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
2xxquickly; do heavy work asynchronously. - Treat delivery as at-least-once and dedupe on the event identity.
- Filter on
X-FabHub-Eventand ignore event types you do not handle.