Fabric documentation

Errors

The problem+json body, the twelve codes, and why two of them tell you little.

Every refusal is application/problem+json (RFC 9457) with the same fields:

{
  "type": "https://fabric.inc/docs/api/errors#insufficient_scope",
  "title": "Credential lacks the required scope",
  "status": 403,
  "code": "insufficient_scope",
  "detail": "This key does not carry `catalog:write`.",
  "instance": "/api/v1/brands/brd_1/products/prd_9"
}

Branch on code. It is a closed set, stable across releases, and it is the only field you should compare. title and detail are written for a person reading a log — detail describes this one occurrence and its wording will change.

A validation failure adds errors, one entry per field, so you can mark the input that was wrong rather than printing a sentence:

{
  "code": "invalid_request",
  "status": 400,
  "errors": [{ "path": "limit", "message": "Expected number, received string" }]
}

Codes

invalid_request

400. The body or query could not be understood. Read errors — it names each field and what was wrong with it. Fix the request; retrying it unchanged returns the same thing.

refused

400. The request was understood and declined on its merits. detail names the reason, and it is a reason about your data rather than your syntax — a mapping that is not active, a run that has nothing to do. Retrying unchanged will not help.

unauthorized

401. No usable credential. See below — every cause returns this same body on purpose.

forbidden

403. The credential is valid and carries the scope, but you may not do this. A scope never substitutes for a role. Three things answer it: changing organization configuration as a member, writing a global config scope (a platform setting, refused to owners too), and an operation your organization is not entitled to, such as Catalog Builder.

Publishing's role checks are the exception. They run inside the operation rather than at the door, so approving a publish, cancelling one, and activating or rolling back a mapping version all answer refused instead. detail names the role either way.

insufficient_scope

403. The credential is valid but does not carry the scope this endpoint requires. detail names the missing scope. Scopes are fixed when the key is created, so this is a new key rather than a retry.

not_found

404. The resource does not exist, or it belongs to another organization. Those two are deliberately indistinguishable — see below.

conflict

409. The request contradicts the current state: reusing an Idempotency-Key with a different body, or starting a run on a brand that already has one going. detail says which.

unprocessable

422. Well-formed and permitted, but it cannot be applied to this resource in its current state — approving a value that no AI wrote, for instance. The request was right; the target was not.

too_large

413. The answer would be too large to return synchronously. A publishing export over the row cap for your organization or brand — 5,000 products by default — is the usual cause. detail carries both the count and the cap. Start a run instead: it streams, resumes and leaves a record.

rate_limited

429. You are over one of the four budgets. Retry-After says how many seconds to wait, and RateLimit-Reset when the window turns over. See Rate limits.

server_error

500. Something broke on our side. Retry with backoff. The body carries no detail on purpose — see below.

service_unavailable

503. We are not configured to serve this yet — a missing credential key on our side, not yours. detail names the configuration, because it describes our system rather than your data. Tell us; retrying will not fix it.

Every auth failure looks the same

A missing key, a malformed key, a key that never existed, a revoked key, an expired key, and a key whose organization was archived all return the same 401 with the same message.

That is deliberate. Distinguishing them would tell a caller which of their guesses was once a real key, which is exactly what someone enumerating keys wants to learn.

Debugging your own integration, the useful checks are: is the header spelled right, is the key the full string including the fab_sk_ prefix, and has it been revoked.

A resource that is not yours is not found

Sending a brandId from another organization returns 404, never 403.

Same reasoning: "that brand exists but is not yours" confirms the id. The API will not tell you whether an id you do not own is real. This applies to every scoped resource — products, views, connections, mappings, runs and webhook endpoints all answer the same way.

500s carry no detail

A server error returns the bare server_error body, because an internal message can name a brand, a destination or a credential and the caller who triggered it should learn none of them. The detail is in our logs, correlated by request.