Errors
Every error the Fibric API returns uses standard HTTP status codes and one consistent JSON envelope. The code is stable and machine-readable; branch on it, never on message, which is human-readable and may change. This page is the complete code table, grouped by status, with the conflict semantics that matter most: idempotency replay and the single-flight lock.
The error envelope
The broad class of failure: invalid_request_error, authentication_error, permission_error, not_found_error, conflict_error, validation_error, rate_limit_error, or api_error. One type per status group; use it for coarse handling.
The specific, stable code, for example entity_locked. New codes may be added within a major version; existing codes never change meaning.
Human-readable detail, naming the field or resource involved. Safe to log; not safe to parse.
A link to the section of this page that documents the code.
Identifies the request in our logs and in any receipt the request produced. Include it when contacting support.
{
"error": {
"type": "conflict_error",
"code": "state_conflict",
"message": "Plan pl_7c1a is already executed and cannot be approved.",
"doc_url": "https://fibric.io/docs/errors#state_conflict",
"request_id": "req_9b3e21f0"
}
}
The envelope is the whole contract: no error returns a bare string, an HTML page, or a different JSON shape, including 429 and 5xx responses. A client that handles the envelope handles every failure the API can produce.
Retryability at a glance
Before the full table, the decision that matters most in a client: given a status, is an unchanged retry ever correct?
| Status | Retry unchanged? | Why |
|---|---|---|
400 401 403 404 422 | No | The request itself is the problem. Sending it again produces the same answer; fix the request, the key, or the policy first. |
409 entity_locked | Yes, after Retry-After | The state that refused you is transient: the lock releases when the in-flight work disposes. |
409 all other codes | No | The state that refused you will not change on its own. Read the resource and act on its actual state. |
429 | Yes, after Retry-After | The request was never processed; the budget refills. |
500 502 503 | Yes, with backoff | Reads are always safe. Writes are safe when they carried an Idempotency-Key, which is why every write should. |
Codes by HTTP status
Bad request
Type invalid_request_error. The request never reached business logic. Do not retry unchanged.
| Code | When it happens | How to fix |
|---|---|---|
invalid_json | The body is not parseable JSON. | Fix the serialization; check for truncation and encoding. |
invalid_request | The body parsed but the request is malformed as a whole, for example a payload over the 256 KB limit or a missing Content-Type. | Follow the detail in message; it names what was wrong. |
missing_parameter | A required field is absent. message names it. | Send the field. Required fields are marked on each endpoint page. |
invalid_parameter | A field is present but fails validation: a non-slug operator name, an event_type that is not dotted noun.verb, a timestamp that is not RFC 3339, a limit outside 1–100. | Correct the named field to the documented format. |
invalid_cursor | A pagination cursor is malformed, expired, or was issued for a different query. | Restart the listing from the first page and re-walk next_cursor. |
Unauthenticated
Type authentication_error. The server does not know who you are. Do not retry with the same credentials.
| Code | When it happens | How to fix |
|---|---|---|
unauthenticated | The Authorization header is missing or is not a Bearer token. | Send Authorization: Bearer <key> on every request. |
key_invalid | The token is not a key the server recognizes. | Check for whitespace and truncation; confirm you are using the right environment's key. |
key_revoked | The key existed but has been revoked. | Mint a new key in the console and rotate your deployment. |
Forbidden
Type permission_error. The server knows who you are and the answer is no.
| Code | When it happens | How to fix |
|---|---|---|
insufficient_scope | The key is valid but lacks the scope the route requires, for example calling plans:approve with a read-only key. | Issue a key with the needed scope. Scopes are listed on each endpoint's route bar. |
tenant_mismatch | The body carried a tenant_id or reseller_id that does not match the key. | Remove the tenancy fields from the body; the server stamps them from the key. There is no cross-tenant key. |
Not found
Type not_found_error.
| Code | When it happens | How to fix |
|---|---|---|
not_found | No such resource for the authenticated tenant. An id that exists in another tenant also reads as not_found: existence is never disclosed across the tenancy wall. | Verify the id and its prefix (ev_, op_, cn_, pl_, act_, rc_, exp_), and that the key belongs to the tenant that owns the resource. |
Conflict
Type conflict_error. The request was well formed but collided with the current state. The two conflict codes with dedicated semantics, idempotency_conflict and entity_locked, are detailed below the table.
| Code | When it happens | How to fix |
|---|---|---|
state_conflict | A lifecycle precondition failed: approving an executed plan, resuming a draft operator, testing a pending_auth connector, or creating a resource whose unique name is taken. | Fetch the resource, check its status, and act on the state it is actually in. |
idempotency_conflict | An Idempotency-Key was replayed with a different request body. | Use a new key for a new request, or replay the original body unchanged. See below. |
entity_locked | The single-flight lock on an entity_key the request needs is held by other in-flight work. | Wait for Retry-After, then retry the same request unchanged. See below. |
connector_in_use | Uninstalling a connector that is the only fulfiller of a capability an active operator depends on. | Pause the dependent operators or install another connector fulfilling the capability, then retry. |
action_not_undoable | Undoing an action whose tool declares no inverse, that never applied, or that was already undone. | Check the action's undoable and undone fields before calling undo. |
Unprocessable
Type validation_error. The request was understood and refused on its merits, usually by governance.
| Code | When it happens | How to fix |
|---|---|---|
policy_blocked | Every action in the request was refused by the fail-closed trust policy. The body carries the verdicts. | This is the guardrail working. To allow the action, change the operator's guardrails in the Operators API; approval never overrides a BLOCK. |
capability_unbound | An operator requests a capability no installed connector fulfills. | Install a connector that fulfills the capability through the Connectors API. |
listing_early_access | Installing a marketplace listing that is early access rather than live. | Request access; early-access listings are provisioned with your team during onboarding. |
auth_failed | Connector credentials were rejected by the source system at install or test. | Re-check the credentials against the source system, then retry the install or test. |
Too many requests
Type rate_limit_error. Budgets, windows, and quota mechanics are documented in Rate limits & quotas.
| Code | When it happens | How to fix |
|---|---|---|
rate_limited | The request rate for the route class exceeded its window. | Back off for Retry-After seconds, then resume. Watch X-RateLimit-Remaining to stay under. |
quota_exceeded | A standing quota is exhausted: the monthly action allowance, or the concurrent export-job cap. | For actions, overage runs at $0.01 per action per your plan settings; raise or lift the cap in the console. For exports, wait for a running job to finish. |
Server errors
Type api_error. Something failed on our side or upstream of us. Retries are safe on any request that carried an Idempotency-Key and on all reads.
| Status | Code | When it happens | How to fix |
|---|---|---|---|
500 | internal_error | An unexpected failure inside the platform. | Retry with exponential backoff. If it persists, contact support with the request_id. |
502 | connector_upstream_error | A connector's source system errored or was unreachable while handling your request. | Check the connector's status and the source system's health; run a connector test. |
503 | service_unavailable | The platform is briefly unable to serve the route, for example during a deploy. | Retry after Retry-After. Ingest retries dedupe on the idempotency key, so nothing double-counts. |
Idempotency conflict semantics
An Idempotency-Key binds a key to the exact request body it was first seen with. Three outcomes are possible on replay, and only one is an error:
| Replay | Result | Meaning |
|---|---|---|
| Same key, same body | 200 with the original response | The safe retry. Nothing executed twice; you receive the stored result of the first attempt. |
| Same key, different body | 409 idempotency_conflict | The key is telling you your client has a bug: two different requests claimed to be the same one. Neither the first result nor the new body is acted on. |
| New key | Normal processing | A genuinely new request. |
Keys are scoped per tenant and route, and retained for 24 hours. Derive keys from the operation's natural identity, for example magento:SO-10884:v7 for an event or approve-pl_7c1a for an approval, rather than from random values, so a crashed-and-restarted worker replays instead of duplicating. Side-effecting plan actions additionally carry their own idempotency_key in the body, enforced by the executor itself; a replayed action disposes as DEDUP and is receipted, not errored. See Single-flight & idempotency.
Single-flight 409 semantics
The executor serializes side effects per entity_key: one thing in flight per order, per room, per asset. Inside a single plan this is invisible, actions on the same entity simply run in order. Across requests, it surfaces as a lock:
HTTP/1.1 409 Conflict
Retry-After: 4
{
"error": {
"type": "conflict_error",
"code": "entity_locked",
"message": "order:SO-10884 is locked by plan pl_7c1a; retry after the in-flight work disposes.",
"doc_url": "https://fibric.io/docs/errors#entity_locked",
"request_id": "req_4d17ab02"
}
}
Handling rules:
- The lock is per entity, not per tenant. Work on other entities proceeds normally while one entity is locked.
- Retry the same request unchanged, with the same
Idempotency-Key, afterRetry-Afterseconds. If the in-flight work already did what your request would do, the idempotency layer dedupes and you get the stored result rather than a second side effect. - Do not treat
entity_lockedas failure in monitoring. It is the kernel refusing to interleave two writes on one entity, which is the behavior that prevents a double hold or a double refund.
Working with request ids
Every response, success or failure, carries the request id, in the body for errors and in the X-Request-Id response header for everything. The same id appears in any receipt the request produced, so one identifier joins your client logs, our server logs, and the audit ledger.
# capture the request id from the error body
curl -s -X POST https://api.fibric.io/v1/plans/pl_7c1a/approve \
-H "Authorization: Bearer sk_live_3f9c2a7b8e1d4f60a2c9" \
-H "Content-Type: application/json" \
-d '{"approver": "l.ops@example.com"}' | jq -r '.error.request_id'
# req_9b3e21f0
# then find any receipt the request wrote
curl -s "https://api.fibric.io/v1/receipts?request_id=req_9b3e21f0" \
-H "Authorization: Bearer sk_live_3f9c2a7b8e1d4f60a2c9"
Log the request id on every non-2xx response as a matter of course. When you contact support, it is the one field that lets us find the exact request without guessing from timestamps.
When governance refuses work, a policy_blocked response or a BLOCK verdict inside a plan, the refusal is written to the receipt ledger like any success. An auditor can see not only what ran but what was refused, when, and by which rule.