Fabric documentation

Publishing over the API

Create a destination connection, test it, start a run, and read what it wrote.

A publishing run sends your enriched catalog to one or more destinations and records what happened to every product. This page walks one destination from nothing to a finished run.

You need an API key carrying publishing:read and publishing:write. Approving a publish to a feed or a storefront also needs the owner role — a member key can do everything here except the final step.

1. Pick a destination

The catalog is the same for every organization and needs no scope beyond a valid key.

curl https://fabric.inc/api/v1/publishing/destinations \
  -H "Authorization: Bearer fab_sk_8Kq2..."
{
  "destinations": [
    {
      "defaultDelivery": "download",
      "destinationKey": "fabric_canonical",
      "fieldCount": 15,
      "grain": "product",
      "kind": "file",
      "needsApproval": false,
      "needsPrice": false,
      "serialization": "csv",
      "supportsDelta": false
    }
  ]
}

Read needsApproval before anything else. It is false only for file destinations, which hand bytes back rather than publishing. Everything else needs an owner's approval, and Destinations says what each one is.

2. Create a connection

A connection is one destination for one brand, with the credential it delivers through.

curl -X POST https://fabric.inc/api/v1/publishing/connections \
  -H "Authorization: Bearer fab_sk_8Kq2..." \
  -H "Content-Type: application/json" \
  -d '{
    "brandId": "brd_4b81de05",
    "destinationKey": "acp_stripe",
    "name": "Stripe — Northwind Outdoors",
    "credential": "sk_live_..."
  }'

The credential is sealed before it is stored and is never returned. Afterwards only its last four characters are readable, which is enough to tell two keys apart and not enough to use one.

{
  "id": "cn_7f3a9c21",
  "destinationKey": "acp_stripe",
  "name": "Stripe — Northwind Outdoors",
  "credentialHint": "9fQ2",
  "hasCredential": true,
  "delivery": { "scheme": "stripe" },
  "enabled": false,
  "lastTestOk": null,
  "lastTestedAt": null,
  "lastTestDetail": null,
  "createdAt": "2026-09-05T14:22:07.183Z"
}

Note

enabled: false is not an error. A new connection is disabled until a test succeeds, so a destination Fabric has never reached cannot be published to by accident.

3. Test it

Testing round-trips the real transport. Passing is what enables the connection.

curl -X POST https://fabric.inc/api/v1/publishing/connections/cn_7f3a9c21/test \
  -H "Authorization: Bearer fab_sk_8Kq2..."
{ "ok": true, "detail": "Stripe accepted this key for catalog imports." }

A failed test answers 200 with "ok": false and a detail saying what went wrong. The connection stays disabled, and a run that needs it delivers nothing — failing here is far cheaper than failing halfway through a publish.

4. Dry-run it

A dry run builds every record and reports what would happen without delivering anything. It is exempt from approval, because seeing what would change is what happens before approving.

curl -X POST https://fabric.inc/api/v1/publishing/runs \
  -H "Authorization: Bearer fab_sk_8Kq2..." \
  -H "Content-Type: application/json" \
  -d '{
    "brandId": "brd_4b81de05",
    "destinationKeys": ["acp_stripe"],
    "mode": "dry_run"
  }'

Scope the run with viewId, listId, or scope.skus. With none of them it covers the brand's whole catalog. mode is full, delta or dry_run; delta sends only what changed and is available on the destinations whose supportsDelta is true.

5. Publish it

The same call with approve: true is the real publish. One call both approves and publishes, and only an owner key may approve one to a feed or storefront. On a file destination the flag is ignored, because a download has nothing to approve.

curl -X POST https://fabric.inc/api/v1/publishing/runs \
  -H "Authorization: Bearer fab_sk_8Kq2..." \
  -H "Content-Type: application/json" \
  -d '{
    "brandId": "brd_4b81de05",
    "destinationKeys": ["acp_stripe"],
    "mode": "full",
    "approve": true
  }'

An owner key publishes without a second step

There is no separate confirmation. An owner-scoped key holding publishing:write can approve its own publish to a live destination in one request. Give integrations that only prepare or read a member key instead.

{
  "runId": "run_2c6f81a4",
  "started": true,
  "workflowId": "publish-run_2c6f81a4"
}

Starting a run is always 202, whether or not the workflow started. The run row exists either way and started says which happened — a 500 here would invite a retry that publishes twice. When started is false the body carries an error and the run is readable at the id you were given.

6. Read the run

Read the run for the outcome, or register a webhook endpoint for publish_run_completed and publish_run_failed and skip the polling.

curl https://fabric.inc/api/v1/publishing/runs/run_2c6f81a4 \
  -H "Authorization: Bearer fab_sk_8Kq2..."
{
  "id": "run_2c6f81a4",
  "brandId": "brd_4b81de05",
  "mode": "full",
  "status": "completed",
  "productCount": 18412,
  "approvedAt": "2026-09-05T14:22:07.183Z",
  "approvedById": null,
  "createdAt": "2026-09-05T14:22:07.183Z",
  "startedAt": "2026-09-05T14:22:08.512Z",
  "finishedAt": "2026-09-05T14:29:41.760Z",
  "error": null,
  "destinations": [
    {
      "destinationKey": "acp_stripe",
      "status": "completed",
      "recordCount": 18412,
      "writtenCount": 18397,
      "skippedCount": 15,
      "failedCount": 0,
      "artifactUri": "https://api.stripe.com/v2/commerce/product_catalog/imports/pci_9fQ2x4",
      "reportKey": "org/org_2f9a1c/brand/brd_4b81de05/publishing/runs/run_2c6f81a4/acp_stripe.jsonl",
      "error": null
    }
  ]
}

A run ends in one of completed, partial, failed or cancelled; pending and running mean it is still going. partial means some destinations finished and others did not, so read destinations rather than the top-level status alone.

approvedById is null when an API key approved the run — a key is not a person. The run still records which key acted, so the audit trail names it.

A product missing something the destination requires is held back and named in the report at reportKey rather than failing the run, so skippedCount is where a partial result shows up.

Only an owner may cancel a running publish, at POST /v1/publishing/runs/{id}/cancel. Stopping a storefront write half-way leaves the store partly updated, so it is the same decision as starting one.

Downloading instead of publishing

A file destination can skip all of this and return the bytes directly.

curl -X POST https://fabric.inc/api/v1/publishing/export \
  -H "Authorization: Bearer fab_sk_8Kq2..." \
  -H "Content-Type: application/json" \
  -d '{ "brandId": "brd_4b81de05", "destinationKey": "fabric_canonical" }' \
  -o catalog.csv

The response carries X-Fabric-Records and X-Fabric-Skipped, so you need not parse the body to count what you got. Above your synchronous row cap the request answers too_large and you start a run instead. A feed or storefront destination is refused here whatever its size: a publish needs the approval and the audit trail this endpoint cannot record.

  • Destinations — every destination, its kind, and the download cap.
  • Authentication — scopes, roles and what a key may do.
  • Errors — every code this surface returns.