Skip to content

Webhooks

Webhooks let your backend receive push notifications for Fabric events instead of polling. Register a URL, choose which events to subscribe to, and Fabric will POST a signed JSON payload every time a matching event fires.

Use webhooks alongside SSE/WebSocket streaming for a complete integration: SSE for real-time UI updates, webhooks for backend-to-backend pipelines, alerting, and async processing.

The signing secret is generated server-side and returned once in the create response. For local development, fabric setup provides it as FABRIC_WEBHOOK_SECRET.

import { FabricClient } from "@fabric-platform/sdk";
const fabric = new FabricClient({ apiKey: "fab_xxx" });
const result = await fabric.webhooks.create(orgId, {
url: "https://your-app.com/fabric/webhook",
event_filter: ["workflow.run.completed", "workflow.run.failed"],
});
// result.secret is "whsec_..." — returned ONCE. Store it securely.
// result.id, result.url, result.active, etc. are the webhook metadata.

Every event has a dotted string kind. Use event_filter to subscribe to specific events — an empty filter receives all events.

EventDescription
workflow.run.createdRun was submitted
workflow.run.queuedRun entered the priority queue
workflow.run.promotedRun was promoted in the queue
workflow.run.startedExecution began
workflow.run.completedRun finished successfully
workflow.run.failedRun failed (check payload.error)
workflow.run.cancelledRun was cancelled by a user or API call
workflow.run.waitingRun is waiting for external input
workflow.run.pausedRun was paused
EventDescription
workflow.node.startedA node within a run started executing
workflow.node.completedA node finished successfully
workflow.node.failedA node failed
workflow.node.progressIntra-node progress (emitted by SDK log.emit() during long-running tasks)
EventDescription
job.createdA single-node job was submitted
job.startedJob execution began
job.completedJob finished successfully
job.failedJob failed
EventDescription
workflow.schedule.triggeredA scheduled workflow was triggered
workflow.schedule.skippedA scheduled trigger was skipped (e.g. concurrency limit)
workflow.schedule.failedA scheduled trigger failed to launch
EventDescription
webhook.delivery.exhaustedA webhook delivery exhausted all retries (dead letter)
organization.createdA new organization was created
invitation.createdAn invitation was sent
invitation.acceptedAn invitation was accepted
invitation.revokedAn invitation was revoked

Every webhook delivery POSTs a JSON DomainEvent:

{
"id": "evt_a1b2c3d4-...",
"kind": "workflow.run.completed",
"organization_id": "org-uuid",
"workflow_id": "wf-uuid",
"run_id": "run-uuid",
"node_key": null,
"payload": {
"sayiir_instance_id": "fab_run-uuid",
"state": { "workspace_id": "xxx", "triggered_by": "yyy" },
"output_url": "https://gofabric.dev/v1/workflows/runs/run-uuid/output"
},
"timestamp": "2026-04-18T12:00:00Z"
}

Fields are null when not applicable — for example, node_key is only set on node-level events, and job_id is only set on job events.

Terminal events (workflow.run.completed, workflow.run.failed, workflow.run.cancelled) include an output_url in the payload. GET this URL (with your API key) to retrieve the workflow output and all artifacts with signed download URLs:

{
"run_id": "run-uuid",
"status": "completed",
"output": { "video_url": "https://..." },
"artifacts": [
{
"asset_id": "asset-uuid",
"filename": "ai-short.mp4",
"content_type": "video/mp4",
"download_url": "https://gofabric.dev/v1/assets/asset-uuid?expires=...&signature=...",
"download_url_expires_at": "2026-04-19T12:00:00Z"
}
]
}

Each download_url is a time-limited signed URL (default 1 hour, configurable via ?expires_in= on the output URL, max 24 hours). No auth headers needed for the download itself.

The SDKs provide convenience methods to handle this automatically:

// Option 1: submit + wait + get output with signed URLs in one call
const result = await fabric.workflows.runs.submitAndGetOutput("video/ai-shorts", {
input: { topic: "AI news" },
});
for (const a of result.artifacts) {
console.log(a.filename, "", a.download_url);
}
// Option 2: download artifact contents directly
const files = await fabric.workflows.runs.downloadAllArtifacts(runId);
for (const f of files) {
fs.writeFileSync(f.filename, Buffer.from(f.data));
}

Important: Workflow-generated assets are automatically cleaned up after 72 hours. Download or mirror assets promptly after receiving the completion event. Fabric is not intended as permanent storage — treat it as a transient delivery mechanism.

When you submit a workflow run with a state field, that opaque JSON object is persisted on the run record and echoed back inside payload.state on all terminal events (workflow.run.completed, workflow.run.failed, workflow.run.cancelled). This lets multi-tenant consumers correlate completions with their own context (e.g. workspace ID, triggering user) without making a follow-up API call.

Terminal window
# Submit with state
curl -X POST https://gofabric.dev/v1/workflows/run?name=research/trend-analyst \
-H "Authorization: Bearer fab_xxx" \
-H "Content-Type: application/json" \
-d '{
"input": { "topic": "AI trends" },
"state": { "workspace_id": "ws-123", "triggered_by": "user-456" }
}'

The state is also visible in the run’s SSE/WebSocket event stream and in the GET /v1/workflows/runs/{id} response.

Every delivery includes these headers:

HeaderDescription
Content-Typeapplication/json
X-Fabric-Signaturesha256=<hex> — HMAC-SHA256 of the raw body
X-Fabric-EventEvent kind (e.g. workflow.run.completed)
X-Fabric-DeliveryUnique delivery/event ID
X-Fabric-RetryAttempt number (present on retries only)

Every delivery is signed with HMAC-SHA256 using the secret you provided at registration. Always verify signatures before processing payloads.

import { constructWebhookEvent, verifyWebhookSignature } from "@fabric-platform/sdk";
// Option 1: verify + parse in one call (recommended)
app.post("/webhook", async (req, res) => {
try {
const event = await constructWebhookEvent(
req.body, // raw body string
req.headers["x-fabric-signature"]!, // sha256=<hex>
process.env.WEBHOOK_SECRET!,
);
switch (event.kind) {
case "workflow.run.completed":
const artifacts = await fabric.workflows.runs.artifactsWithUrls(event.run_id!);
await processOutput(artifacts);
break;
case "workflow.run.failed":
await alertOncall(event.payload);
break;
}
res.sendStatus(200);
} catch {
res.status(401).send("Invalid signature");
}
});
// Option 2: verify separately
const valid = await verifyWebhookSignature(rawBody, signature, secret);

Webhooks support an opaque state object (up to 16 KB) that is echoed back in every delivery. This is separate from the per-run state (submitted with the workflow) — consumer state lives on the webhook subscription itself and is included in every delivery regardless of which run triggered it.

Use this to attach your own context — workspace IDs, environment tags, external correlation IDs — so your receiver can route events without a database lookup.

const result = await fabric.webhooks.create(orgId, {
url: "https://your-app.com/fabric/webhook",
event_filter: ["workflow.run.completed", "workflow.run.failed"],
state: {
workspace_id: "ws-123",
env: "production",
callback_queue: "content-pipeline",
},
});

When consumer state is set, deliveries wrap the event in an envelope with both the event and the state:

{
"event": {
"id": "evt_...",
"kind": "workflow.run.completed",
"run_id": "run-uuid",
"payload": { "output": { "..." : "..." } }
},
"state": {
"workspace_id": "ws-123",
"env": "production",
"callback_queue": "content-pipeline"
}
}

Without consumer state, the raw event is delivered directly (no wrapping envelope).

Consumer state can be updated via PATCH /v1/webhooks/{id} without re-creating the webhook.

Beyond event type filtering, you can scope a webhook to specific resources using resource_filter. All specified keys must match (AND logic):

{
"url": "https://your-app.com/webhook",
"secret": "whsec_...",
"event_filter": ["workflow.run.completed", "workflow.run.failed"],
"resource_filter": {
"workflow_definition_id": "wf-uuid-here"
}
}

Supported filter keys:

KeyDescription
workflow_definition_idOnly events for runs of this workflow definition
workflow_run_idOnly events for this specific run
job_idOnly events for this specific job

An empty or absent resource_filter matches all events of the subscribed types.

Fabric delivers webhooks with at-least-once semantics:

  • Timeout: 10 seconds per attempt. Your endpoint must respond with a 2xx status within this window.
  • Retries: Configurable via max_retries (default 3). Failed deliveries retry with exponential backoff: 30s, 60s, 120s, 240s, … (30s × 2attempt, capped at ~8.5 hours).
  • Dead letter: After exhausting all retries, a webhook.delivery.exhausted event is emitted. Subscribe to this event on a separate webhook to build alerting.
  • Idempotency: Use the X-Fabric-Delivery header (event ID) to deduplicate — the same event may be delivered more than once on retries.
const deliveries = await fabric.webhooks.deliveries(webhookId);
for (const d of deliveries) {
console.log(d.event_kind, d.status, d.status_code, d.attempt, d.error_message);
}

Each delivery record includes:

FieldDescription
idUnique delivery ID
webhook_idThe webhook subscription this delivery belongs to
event_idThe domain event ID
event_kindEvent type string
statusdelivered, failed, or pending
status_codeHTTP response code (if received)
error_messageError details (on failure)
attemptCurrent attempt number
created_atWhen the delivery was first attempted
// List all webhooks for an org
const webhooks = await fabric.webhooks.list(orgId);
// Get a single webhook
const wh = await fabric.webhooks.get(webhookId);
// Update (pause, change filters, etc.)
await fabric.webhooks.update(webhookId, {
active: false,
description: "Paused for maintenance",
});
// Re-activate with new filters
await fabric.webhooks.update(webhookId, {
active: true,
event_filter: ["workflow.run.completed"],
resource_filter: { workflow_definition_id: "wf-uuid" },
});
// Delete
await fabric.webhooks.delete(webhookId);

Webhook secrets follow the Stripe pattern — the secret is generated server-side and returned once at creation time. It cannot be retrieved again via API.

How secrets work:

  1. Generation — When you create a webhook, Fabric generates a cryptographically secure 32-byte random key, formatted as whsec_<64-hex-chars>.
  2. One-time return — The secret field appears only in the create response. Store it immediately in a secure location (environment variable, secrets manager, encrypted database column).
  3. Signing — On every delivery, Fabric computes HMAC-SHA256(secret, raw_json_body) and sends the result as X-Fabric-Signature: sha256=<hex>.
  4. Verification — Your receiver recomputes the HMAC with the stored secret and compares using constant-time comparison to prevent timing attacks.

If you lose the secret:

The secret cannot be recovered. Delete the webhook and create a new one — Fabric issues a fresh secret. Update your verification code with the new value.

To rotate a webhook secret, delete the existing webhook and create a new one with a fresh secret. The old webhook stops receiving deliveries immediately.

MethodPathDescription
POST/v1/organizations/{org_id}/webhooksCreate a webhook
GET/v1/organizations/{org_id}/webhooksList webhooks for an org
GET/v1/webhooks/{id}Get a webhook
PATCH/v1/webhooks/{id}Update a webhook
DELETE/v1/webhooks/{id}Delete a webhook (soft)
GET/v1/webhooks/{id}/deliveriesList delivery attempts
  1. Always verify signatures. Never trust a webhook payload without HMAC verification. Use constructWebhookEvent() / construct_event() which combines verification and parsing.

  2. Respond quickly. Return a 2xx within the 10-second timeout. If you need to do heavy processing, enqueue the work and respond immediately.

  3. Handle duplicates. Use the X-Fabric-Delivery event ID to deduplicate. At-least-once delivery means you may receive the same event more than once.

  4. Use event filtering. Subscribe only to the events you need. This reduces delivery volume and avoids unnecessary processing.

  5. Monitor delivery health. Check the delivery log for persistent failures. Subscribe to webhook.delivery.exhausted on a separate, reliable endpoint to catch dead letters.

  6. Rotate secrets periodically. Delete the existing webhook and create a new one with a fresh secret. Update your verification code with the new secret.

  7. Use resource filters for multi-tenant setups. If you run multiple workflows, scope webhooks with resource_filter to avoid cross-workflow noise.

  8. Download artifacts promptly. Workflow-generated assets expire after 72 hours (signed download URLs expire after 1 hour). Mirror artifacts to your own storage on workflow.run.completed — don’t defer. See Asset Lifecycle for details.

  9. Use consumer state for multi-tenant routing. Attach a state object (workspace ID, environment, callback queue) to the webhook so your receiver can route events without a database lookup.