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.csvThe 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.
Related
- Destinations — every destination, its kind, and the download cap.
- Authentication — scopes, roles and what a key may do.
- Errors — every code this surface returns.