Fabric documentation

Receiving webhooks

Get a signed request when a job finishes, and verify its signature.

A webhook endpoint receives a signed POST when a job reaches a terminal state. With an endpoint registered, Fabric tells you an import completed, so you do not have to poll for it every thirty seconds.

Register an endpoint

curl -X POST "https://fabric.inc/api/v1/brands/$BRAND/webhooks" \
  -H "Authorization: Bearer $FABRIC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/fabric","events":["import_completed"]}'
{
  "id": "whe_...",
  "url": "https://example.com/hooks/fabric",
  "secret": "whsec_...",
  "secretHint": "9fQ2"
}

The secret appears once

The secret is returned only in this response, and no endpoint reads it back. If you lose it, call POST /v1/brands/{brandId}/webhooks/{webhookId}/rotate-secret to issue a new one. The old secret stops verifying immediately, so have your receiver ready for the new secret before you rotate.

Omit events to receive every type. The URL must be public HTTPS. Loopback and private addresses are refused.

Events

EventSent when
import_completed · import_failedAn import finished, or gave up
enrichment_run_completed · enrichment_run_failedAn enrichment run finished, or gave up
export_readyAn export's artifact is ready to fetch
publish_run_completed · publish_run_failedA publish finished, or gave up

Request format

POST /hooks/fabric
Content-Type: application/json
Fabric-Event-Id: evt_01J8...
Fabric-Event-Type: import_completed
Fabric-Signature: t=1757260800,v1=5257a869...
{
  "id": "evt_01J8...",
  "type": "import_completed",
  "createdAt": "2026-09-07T18:00:00.000Z",
  "data": { "importJobId": "imp_...", "productCount": 2431 }
}

Verify the signature

Compute HMAC-SHA256 over "<t>.<raw body>" with your secret and compare it to v1. Use the raw body, before any JSON parsing. Re-serializing changes the bytes, and the signature will not match.

import { createHmac, timingSafeEqual } from "node:crypto";

const verify = (rawBody: string, header: string, secret: string): boolean => {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("=") as [string, string])
  );
  // Reject a stale delivery before spending a hash on it.
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) {
    return false;
  }
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && timingSafeEqual(a, b);
};

Compare in constant time and reject anything older than 300 seconds. The timestamp is part of the signed material, so a captured delivery cannot be replayed later without breaking the signature.

Delivery

Delivery is at-least-once, so the same event can arrive twice. Deduplicate on Fabric-Event-Id.

Return a 2xx to accept a delivery. Any other response is retried eight times over about an hour, with a longer wait each time. Respond first and process the event afterwards: a delivery that takes longer than 10 seconds is abandoned and retried.

An endpoint that fails 20 deliveries in a row is disabled. Any success resets the count, so an endpoint that fails now and then stays enabled, and only one that has stopped responding is disabled. A disabled endpoint is not deleted. It keeps the reason it was disabled, and POST /v1/brands/{brandId}/webhooks/{webhookId}/enable restarts delivery once you have fixed it.

Check delivery attempts

GET /v1/brands/{brandId}/webhooks/{webhookId}/deliveries returns every attempt, with the status code and reply your endpoint returned and how long it took. Check it to confirm Fabric sent the event before you search your own logs.