Receive events with webhooks
Fibric can push to you as well as be polled. Webhook endpoints receive plan proposals, action dispositions, and connector status changes as they happen, each delivery signed with an HMAC in the Fibric-Signature header. This guide registers an endpoint, explains the delivery contract, and gives you a Node handler that verifies signatures with a timing-safe compare.
The webhook management endpoints (whk_ ids) are newer than the core chain and gain additive fields more often. Everything on this page, the CLI verbs and the HTTP endpoints, is stable within the 1.x line. The delivery contract itself, the payload shapes, the signature scheme, and the retry semantics, is stable. See the webhooks note on the API overview.
What gets delivered
Deliveries carry the same objects the pull APIs define; a webhook is a push channel, not a different data model. Each delivery wraps exactly one object in a per-event delivery envelope, described below.
| Delivery type | Fires when | Payload object |
|---|---|---|
plan.proposed | An operator proposes an execution plan. | The plan object from the Actions & plans API. |
plan.awaiting_approval | A plan's policy evaluation is ALERT and it is parked for a human. | The plan object, with its pending disposition. |
action.disposed | The executor disposes an action: applied, blocked, or deduped. | The action object from the Actions & plans API. |
connector.status_changed | An installed connector changes health, for example ready to degraded. | The connector object from the Connectors API. |
Note what is absent: raw event ingest is not fanned out over webhooks. If you want the event stream itself, tail it with fibric events tail or page through the Events API with cursor pagination; webhooks exist to tell you when Fibric decided or did something, not to mirror every envelope.
Prerequisites
- A Fibric workspace and the CLI installed and authenticated; see Installation.
- An HTTPS endpoint you control that can receive
POSTrequests. Plain HTTP endpoints are refused at registration. - Node 20 or newer if you use the example handler below.
Register an endpoint
Register with the CLI. You choose which delivery types the endpoint receives; an endpoint subscribed to nothing receives nothing, and you can add types later without re-registering.
# register an endpoint for plan and action deliveries
fibric webhooks add https://ops.example.com/fibric/hooks \
--types plan.proposed,plan.awaiting_approval,action.disposed
# list registered endpoints
fibric webhooks ls
The whsec_ signing secret is displayed at registration and never again. Store it in your secret manager, not in code. If you lose it, rotate with fibric webhooks rotate whk_4e1a92, which issues a new secret and keeps the old one valid for 24 hours so you can deploy the change without dropping verifications.
The same registration is available over the preview HTTP endpoints. They live under the standard base URL and follow the API's conventions, including the Idempotency-Key header on POST, but as preview endpoints they sit outside the frozen surface:
curl https://api.fibric.io/preview/webhooks \
-H "Authorization: Bearer $FIBRIC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: register-ops-hooks" \
-d '{
"url": "https://ops.example.com/fibric/hooks",
"types": ["plan.proposed", "plan.awaiting_approval", "action.disposed"]
}'
Delivery semantics
The contract has three parts, and your handler should be written against all three.
- At-least-once. Every delivery is attempted until your endpoint acknowledges it or the retry schedule is exhausted. A delivery can therefore arrive more than once, after a timeout on your side, a crash between processing and responding, or a retry racing a slow first attempt. Deduplicate on
delivery_id, or better, make your processing idempotent the same way Fibric's own actions are; see Single-flight & idempotency. - Ordering is not guaranteed. Deliveries are independent requests. A
action.disposedcan arrive before theplan.proposedthat produced it. Usecreated_aton the wrapped object, andcorrelation_idto thread related deliveries, rather than relying on arrival order. - One object per delivery. There is no batching. Each request body is one delivery envelope wrapping one object, so a 2xx from you acknowledges exactly one thing.
Your endpoint acknowledges by returning any 2xx status within 10 seconds. Anything else, a non-2xx, a timeout, a connection failure, counts as a failed attempt and schedules a retry. Respond first and process afterward if your processing is slow; a queue on your side is cheaper than relying on retries.
The delivery payload
Each request body is a delivery envelope: identity and type at the top, the API object in data. Tenancy fields are present on every delivery, the same law that puts reseller_id and tenant_id on every event envelope and every row.
{
"delivery_id": "whd_7f3b21",
"object": "webhook_delivery",
"type": "action.disposed",
"reseller_id": null,
"tenant_id": "t_8f2ac901",
"created_at": "2026-07-02T16:02:44Z",
"data": {
"id": "ac_2d91e0",
"object": "action",
"plan_id": "pl_7c1a",
"connector": "kustomer",
"tool": "calls.status-sync",
"entity_key": "contact:7c1f03ba",
"idempotency_key": "amazon-connect:7c1f03ba:status-sync",
"disposition": "applied",
"receipt_id": "rc_9e12",
"correlation_id": "co_7c1f03",
"created_at": "2026-07-02T16:02:43Z"
}
}
The object in data is exactly what GET /actions/{action_id} would return; nothing exists in a webhook that you cannot also fetch. Follow receipt_id into the receipt ledger for the full proposal-to-disposition record.
Retries and backoff
Failed deliveries retry on an exponential backoff schedule with jitter, up to eight attempts over roughly 21 hours. After the final failure the delivery is marked dead and the endpoint's failure count rises; an endpoint that only fails for 24 hours is automatically disabled, and you re-enable it after fixing your side.
| Attempt | Delay after previous failure | Elapsed, approximate |
|---|---|---|
| 1 | immediate | 0 |
| 2 | 30 seconds | 30 s |
| 3 | 2 minutes | 2.5 min |
| 4 | 10 minutes | 13 min |
| 5 | 30 minutes | 43 min |
| 6 | 2 hours | 2.7 h |
| 7 | 6 hours | 8.7 h |
| 8 | 12 hours | 20.7 h |
Each delay carries up to 20 percent random jitter so a fleet of failed deliveries does not retry in lockstep. Retries reuse the same delivery_id and the same body; only the signature timestamp changes, because each attempt is signed fresh.
Verify signatures
Every delivery carries a Fibric-Signature header. Verify it before trusting the body; an unverified webhook endpoint is an unauthenticated write API into your systems.
Fibric-Signature: t=1751472164,v1=5f8b2c19e4a7d3…9c01
The format is two comma-separated pairs: t, the Unix timestamp (seconds) of the signing moment, and v1, a lowercase hex HMAC-SHA256 of the string ${t}.${body}, keyed with your whsec_ secret, where body is the raw request body, byte for byte, before any JSON parsing. To verify:
- Parse
tandv1out of the header. - Reject if
tis more than five minutes from your clock; this bounds replay of a captured delivery. - Compute HMAC-SHA256 over
${t}.${body}with your secret and compare againstv1using a constant-time comparison, never==.
The HMAC covers the exact bytes on the wire. If your framework parses JSON before your handler runs, re-serializing the object will almost never reproduce those bytes, and verification will fail intermittently. Capture the raw body, verify, then parse. The express example below does this with the verify hook.
Example handler, Node and express
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.FIBRIC_WEBHOOK_SECRET!; // the whsec_… value
const TOLERANCE_SECONDS = 300;
function verifySignature(header: string | undefined, rawBody: Buffer): boolean {
if (!header) return false;
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=", 2) as [string, string]),
);
const t = Number(parts["t"]);
const v1 = parts["v1"];
if (!Number.isFinite(t) || !v1) return false;
// bound replay: reject stale or future timestamps
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;
// hmac-sha256 over `${t}.${body}`, hex-encoded
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${t}.`)
.update(rawBody)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(v1, "hex");
// constant-time compare; length check first, timingSafeEqual throws on mismatch
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
// keep the raw bytes: the HMAC covers the wire body, not re-serialized JSON
app.use(express.json({
verify: (req, _res, buf) => { (req as any).rawBody = buf; },
}));
app.post("/fibric/hooks", (req, res) => {
const ok = verifySignature(
req.header("Fibric-Signature"),
(req as any).rawBody,
);
if (!ok) return res.status(401).end();
const delivery = req.body;
// at-least-once: drop duplicates by delivery_id before doing work
if (alreadySeen(delivery.delivery_id)) return res.status(200).end();
// acknowledge fast, process after; retries are not a work queue
enqueue(delivery);
res.status(200).end();
});
app.listen(8080);
alreadySeen and enqueue are yours to implement: a Redis set with a 48-hour TTL and any job queue will do. The shape matters more than the parts, verify, deduplicate, acknowledge, then work.
Send a test delivery
The CLI can fire a signed test delivery at a registered endpoint, so you can confirm verification end to end before real traffic depends on it:
fibric webhooks test whk_4e1a92 --type action.disposed
Recent deliveries and their attempt history are visible per endpoint with fibric webhooks deliveries whk_4e1a92, including the response code your endpoint returned on each attempt.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Verification fails intermittently | You are HMAC-ing a re-serialized body instead of the raw bytes. | Capture the raw body before JSON parsing, as in the express verify hook above. |
| Verification always fails | Wrong secret, often after a rotation, or the ${t}. prefix is missing from the signed string. | Confirm the whsec_ value in your secret manager and that you concatenate timestamp, a dot, then the body. |
| Deliveries rejected with your own 401 after deploys | Clock skew on the new host trips the timestamp tolerance. | Sync NTP; keep the tolerance at five minutes rather than tightening it. |
| Duplicate processing downstream | Handler does work before acknowledging, so a timeout triggers a retry of work that already ran. | Deduplicate on delivery_id and acknowledge before processing. |
| Endpoint disabled automatically | It failed every attempt for 24 hours. | Fix the endpoint, then re-enable with fibric webhooks enable whk_4e1a92. Dead deliveries are not replayed automatically; fetch the missed window from the pull APIs. |
Next steps
- API overview: conventions, idempotency, and the versioning register this preview surface sits beside.
- Events API: pull the event stream itself, with cursor pagination.
- Monitor your operators: webhooks are one input to a monitoring setup; this guide covers the rest.
- Receipts & audit: every disposition a webhook tells you about has a receipt behind it.
- Connect Amazon Connect and Connect Kustomer: connect the systems whose plans and actions you will hear about.