These docs describe the target Fibric platform contract and proposed developer experience. Public Fibric CLI, API endpoints, and SDK packages are not yet available. All present-tense API, CLI, SDK, tenancy, security, retention, retry, and pricing language below is normative target language, not a statement that the surface is callable or commercially available today. Commands, package names, URLs, responses, limits, and timelines are reference examples unless a page explicitly identifies a deployed BearScope path.
Idempotency
This page specifies a proposed Idempotency-Key contract. Stable keys can suppress recognized retries inside the target service boundary; they do not make networks universally safe or guarantee exactly-once effects in connectors and downstream systems. Retention values below are reference defaults, not public commitments.
Shared conventions, including authentication, pagination, and the error envelope, are defined in the API overview. The conceptual treatment is in Single-flight & idempotency.
The Idempotency-Key header
Send an Idempotency-Key header on any POST. The value is a string of your choosing, 1–255 characters. GET and DELETE requests are idempotent by definition and ignore the header; PATCH accepts it and applies the same replay semantics.
curl -X POST https://api.fibric.io/v1/events \
-H "Authorization: Bearer $FIBRIC_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: magento:SO-10884:v7" \
-d '{"source": "magento", "event_type": "order.updated", "payload": {"order": "SO-10884"}}'
The header is optional on most routes but should be treated as mandatory practice: every write that a retry could duplicate deserves a key. Ingest and approval examples throughout these docs carry one for that reason.
Replay semantics
The first request to arrive with a given key binds that key to its exact request body and, once processing finishes, to its result. Every subsequent request with the same key is a replay. Three outcomes are possible, 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, including its original request_id. |
| Same key, different body | 409 idempotency_conflict | Two different requests claimed to be the same one, which means your client has a bug. Neither the stored result nor the new body is acted on. |
| New key | Normal processing | A genuinely new request. |
Body comparison is byte-exact on the canonicalized JSON: key order does not matter, whitespace does not matter, but a changed value, an added field, or a removed field makes the body different. Headers other than Idempotency-Key itself do not participate in the comparison.
Concurrent replays
If a replay arrives while the first attempt is still processing, the replay does not run the operation a second time and does not wait indefinitely. It fails fast with 409 and code idempotency_in_progress, carrying a Retry-After header. Retry the same request unchanged after the interval; by then the first attempt has finished and you receive its stored result.
HTTP/1.1 409 Conflict
Retry-After: 2
{
"error": {
"type": "conflict_error",
"code": "idempotency_in_progress",
"message": "A request with this Idempotency-Key is still processing; retry unchanged after Retry-After.",
"doc_url": "https://www.fibric.io/docs/api-idempotency#concurrent-replays",
"request_id": "req_2c88f014"
}
}
This is the API-layer cousin of the executor's single-flight lock: both refuse to interleave duplicate work, and both resolve with an unchanged retry.
Scope and retention
- Scope. Keys are scoped per tenant and per route. The same key value on
POST /eventsandPOST /operatorsnames two independent idempotency records; two tenants can use identical key values without interference. - Retention. A managed deployment must define the service-boundary key window in writing. Any duration shown in examples is non-contractual.
- Errors are stored too. If the first attempt failed with a
4xx, the failure is the stored result, and a same-body replay returns the same error without reprocessing. A5xxor a network failure before the server accepted the request stores nothing; the retry processes normally.
The target design separates the HTTP replay window from the action key. Neither layer creates an unconditional lifetime lock: retention, key stability, connector behavior, and downstream reconciliation define the actual boundary.
Deriving good keys
Derive keys from the operation's natural identity, not from random values. A random UUID generated per attempt defeats the mechanism: the retry gets a new key and duplicates the work. The key should answer "which real-world operation is this?" so that any process, including a crashed-and-restarted one, derives the same key for the same operation.
| Operation | Key pattern | Example |
|---|---|---|
| Ingesting a source webhook | source:entity:version | magento:SO-10884:v7 |
| Approving a plan | approve-plan_id | approve-pl_7c1a |
| Provisioning a resource | purpose-name | op-create-order-risk |
| A scheduled job's write | job:window | reconcile:2026-07-02T15 |
Two cautions. First, the key must change when the intended operation changes: ingesting version 8 of the same order needs a new key, hence the :v7 suffix. Second, the key must not change when only the attempt changes: no timestamps of the attempt, no retry counters.
Relation to kernel idempotency keys
Fibric has two idempotency layers, and they protect against different duplicates:
HTTP Idempotency-Key header | Kernel idempotency_key field | |
|---|---|---|
| Protects against | Duplicate requests: network retries, crashed clients, webhook storms. | Duplicate side effects: the same action proposed again in a new plan, by a new request, or after the header window. |
| Lives | In the request headers, set by you. | In each PlannedAction body, set by the proposer, enforced by the deterministic executor. |
| Scope | Per tenant and route; window set by deployment. | Per tenant within the configured executor retention boundary. |
| On duplicate | Returns the stored HTTP response. | Disposes the action as DEDUP, ok: true, no side effect, receipt written. |
The layers compose. Replay a plan approval with the same header key and you get the stored approval response. Approve a different plan that happens to contain an action whose kernel idempotency_key was already applied, and that one action disposes as DEDUP while the rest of the plan proceeds. Either way the side effect happens once, and the receipt ledger records exactly what was deduplicated and when.
Alongside both layers, the executor serializes side effects per entity_key, one thing in flight per order, per room, per asset, which is what surfaces as 409 entity_locked across requests. The three mechanisms together are specified in Single-flight & idempotency.
A correct retry loop
async function post(path: string, body: unknown, idempotencyKey: string) {
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch(`https://api.fibric.io/v1${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FIBRIC_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey, // same key on every attempt
},
body: JSON.stringify(body), // same body on every attempt
});
if (res.status < 500 && res.status !== 409 && res.status !== 429) return res;
const err = res.status === 409 ? (await res.clone().json()).error : null;
if (err && err.code === "idempotency_conflict") return res; // client bug: do not retry
const wait = Number(res.headers.get("Retry-After")) || 2 ** attempt;
await new Promise((r) => setTimeout(r, wait * 1000));
}
throw new Error("retries exhausted");
}
The two invariants the loop must hold: the key and the body are identical on every attempt, and idempotency_conflict is surfaced rather than retried, because it means the caller broke the first invariant.
Errors
| Status | Code | When | Retry? |
|---|---|---|---|
400 | invalid_parameter | The header value is empty or longer than 255 characters. | No; fix the key. |
409 | idempotency_conflict | The key was replayed with a different body. See Errors. | No; this is a client bug. |
409 | idempotency_in_progress | The first attempt with this key is still processing. | Yes, unchanged, after Retry-After. |
409 | entity_locked | The single-flight lock on the target entity_key is held. See Errors. | Yes, unchanged, after Retry-After. |