Webhooks API
Webhooks push the loop to you as it happens: plan proposals, action dispositions, and connector status changes arrive at your HTTPS endpoint within seconds, each delivery signed so you can prove it came from Fibric. This page documents endpoint CRUD, subscription types, the delivery object, the retry schedule, and signature verification. Webhook management is part of the v1.0 surface; the delivery payloads are the same objects defined on the other reference pages.
Shared conventions, including authentication, pagination, the Idempotency-Key header, and the error envelope, are defined in the API overview. Error codes are catalogued in Errors.
Subscription event types
An endpoint subscribes to one or more delivery types. Each delivery wraps the full current API object named in the table, so a consumer needs no follow-up fetch for the common cases.
| Type | Fires when | data contains |
|---|---|---|
plan.proposed | An operator proposes an execution plan. | The plan object, verdicts included. |
plan.disposed | A plan finishes executing, is vetoed, or expires. | The plan object in its final state. |
action.disposed | The executor disposes one action: allowed, alerted, blocked, or deduplicated. | The action object. |
action.needs_approval | An action's verdict is ALERT and a human must approve. | The action object, plus its plan_id for the approval call. |
connector.status_changed | A connector transitions between connected, degraded, error, and pending_auth. | The connector object. |
receipt.written | Any receipt is appended, refusals included. | The receipt object. High volume; subscribe deliberately. |
Types follow the API's noun.verb convention, and new types are additive within the major version; ignore types you do not recognize.
The webhook endpoint object
Unique identifier, prefixed whk_.
Always webhook_endpoint.
The HTTPS destination. Plain HTTP is rejected at creation.
Subscribed delivery types from the table above.
active, disabled (by you), or suspended (by Fibric, after sustained delivery failure; see retries).
Signing secret, prefixed whsec_. Returned in full at creation and on rotation only; reads return a hint. Used to verify Fibric-Signature.
RFC 3339 timestamp of creation.
Timestamp of the most recent delivery attempt, successful or not.
Create an endpoint
webhooks:writeThe HTTPS destination.
One or more delivery types.
curl -X POST https://api.fibric.io/v1/webhook_endpoints \
-H "Authorization: Bearer $FIBRIC_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: whk-create-ops-consumer" \
-d '{
"url": "https://ops.example.com/fibric/webhook",
"types": ["plan.proposed", "action.needs_approval", "connector.status_changed"]
}'
{
"id": "whk_6b3e0d",
"object": "webhook_endpoint",
"url": "https://ops.example.com/fibric/webhook",
"types": ["plan.proposed", "action.needs_approval", "connector.status_changed"],
"status": "active",
"secret": "whsec_5f2e8a1c9b0d7e4f6a3b",
"created_at": "2026-07-02T15:30:00Z",
"last_delivery_at": null
}
Error cases:
| Status | Code | When |
|---|---|---|
400 | invalid_parameter | url is not HTTPS, or a type is unknown. |
409 | state_conflict | An active endpoint with this url already exists. |
List endpoints
webhooks:readReturns the tenant's endpoints, newest first, cursor-paginated with the standard limit and cursor parameters. The secret reads as a hint.
Update an endpoint
webhooks:writeUpdates url, types, or status (active / disabled). Setting status: "active" on a suspended endpoint re-enables it and resets the failure counter. Pass "rotate_secret": true to mint a new signing secret; the response carries the new secret in full, and the old secret remains valid for 24 hours so consumers can roll over without dropped verifications.
curl -X PATCH https://api.fibric.io/v1/webhook_endpoints/whk_6b3e0d \
-H "Authorization: Bearer $FIBRIC_KEY" \
-H "Content-Type: application/json" \
-d '{"rotate_secret": true}'
Delete an endpoint
webhooks:writeDeletes the endpoint. Pending retries for it are dropped; delivery records remain readable for their retention window.
The delivery object
Every attempt to reach your endpoint is recorded as a delivery. The same shape is what arrives in the HTTP POST body.
Unique identifier, prefixed dlv_. Stable across retries of the same delivery: use it as your deduplication key.
Always webhook_delivery.
The endpoint this delivery targets.
The delivery type, for example plan.proposed.
The full API object the type names, at the moment the delivery was created.
The envelope correlation id threading this delivery to its event, plan, and receipts.
1-based attempt counter for this delivery.
pending, succeeded, or failed (retries exhausted). Read over the API only; the POST body carries the delivery mid-flight.
RFC 3339 timestamp the delivery was created.
POST /fibric/webhook HTTP/1.1
Content-Type: application/json
Fibric-Signature: t=1782064081,v1=6e8a2c91f0b34d7e5a1c9f82d4b60e3a7c5f9b1d8e2a4c6f0b3d5e7a9c1f4b68
{
"id": "dlv_9c4f2e",
"object": "webhook_delivery",
"endpoint_id": "whk_6b3e0d",
"type": "action.needs_approval",
"correlation_id": "co_51d2e8",
"attempt": 1,
"created_at": "2026-07-02T15:02:10Z",
"data": {
"id": "act_91d1",
"object": "action",
"plan_id": "pl_7c1a",
"tool": "order.notify",
"entity_key": "order:SO-10884",
"disposition": "ALERT"
}
}
List deliveries
webhooks:readReturns the endpoint's deliveries, newest first, cursor-paginated. Filter with status and type query parameters. Deliveries are retained for 30 days. To replay one, POST to /v1/webhook_endpoints/{endpoint_id}/deliveries/{delivery_id}/redeliver, which re-sends the original payload with a fresh signature.
Retries and backoff
A delivery succeeds when your endpoint returns any 2xx within 10 seconds. Anything else, a non-2xx, a timeout, a connection failure, schedules a retry with exponential backoff:
| Attempt | Delay after previous | Elapsed (approx.) |
|---|---|---|
| 1 | immediate | 0 |
| 2 | 30 seconds | 30 s |
| 3 | 2 minutes | 2.5 m |
| 4 | 10 minutes | 13 m |
| 5 | 1 hour | 1.2 h |
| 6 | 6 hours | 7 h |
| 7 (final) | 24 hours | 31 h |
Delays carry jitter to avoid thundering herds. After the final attempt the delivery is marked failed and remains listable for the retention window. If every delivery to an endpoint has failed for 3 consecutive days, the endpoint is suspended and a notice goes to the tenant's owners; re-enable it with a PATCH once the consumer is healthy, then redeliver what you missed.
Retries and redelivery mean your consumer can see the same dlv_ id twice; deduplicate on it. Ordering across deliveries is not guaranteed, especially under retry. Treat each delivery as a pointer to current state: the data snapshot is convenient, but for decisions, re-read the object by id.
Verifying Fibric-Signature
Every delivery carries a Fibric-Signature header: a Unix timestamp and one or more HMAC-SHA256 signatures, hex-encoded.
Fibric-Signature: t=1782064081,v1=6e8a2c91f0b34d7e5a1c9f82d4b60e3a7c5f9b1d8e2a4c6f0b3d5e7a9c1f4b68
The signed message is the timestamp, a period, and the raw request body: {t}.{body}. To verify: compute HMAC-SHA256 over that message with your whsec_ secret, compare in constant time against each v1 value (two are present during a secret rotation), and reject if the timestamp is older than your tolerance, five minutes is conventional, to defeat replay.
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyFibricSignature(header: string, rawBody: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // 5-minute tolerance
const expected = createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
const given = Buffer.from(String(parts.v1 ?? ""), "hex");
const want = Buffer.from(expected, "hex");
return given.length === want.length && timingSafeEqual(given, want);
}
Verify against the raw body bytes, before any JSON parsing or re-serialization; a re-stringified body will not match. Respond 2xx quickly and do the work asynchronously: the 10-second window includes your processing time.
Errors
| Status | Code | When |
|---|---|---|
400 | invalid_parameter | A non-HTTPS url, an unknown delivery type, or an invalid status transition. |
403 | insufficient_scope | The key lacks the webhooks:read or webhooks:write scope the route requires. |
404 | not_found | No endpoint or delivery with this id exists for the authenticated tenant. |
409 | state_conflict | Creating a duplicate url, or redelivering a delivery that is still pending. |