Skip to content

Idempotency and conflicts ​

The operation key is what makes a lost response, a repeated callback or a concurrent retry converge on one action and one side effect. Get it right before you write any retry logic.

Developer guide reference text ​

On submit, send a stable operation key and the material fields the application will commit. A replay with the same key and same request returns the existing action. Reusing it for a changed amount, beneficiary, tenant or other material parameter returns a conflict without altering the record. Duplicate notifications cause a status read. A target timeout after dispatch leads to reconciliation on the original operation key.

The rest of this page is how the code implements each sentence, and the one place it differs.

What the key identifies ​

An operation key is unique per tenant and service: Axiom looks up an existing action by (tenant, service, operation_key). The tenant is always your authenticated agent's tenant, so two customers can use the same key string without colliding.

The request it is compared against is the canonical action (axiom.action.v1): tenant, agent, audience (service), operation, resource, the normalised material params, not_after and, for a delegated child, the root action. Its SHA-256 over the RFC 8785 canonical JSON is the request_digest, the same digest an approver's decision is bound to.

Two consequences of normalisation:

  • "25000", "25000.0" and 25000 produce the same digest for a decimal field; "usd" and "USD" the same for a currency field.
  • If you omit not_after, a replay reuses the original action's not_after, so the digest stays stable across retries. If you send an explicit not_after, send the same value every time.

Non-material inputs do not affect the digest: intent, payload and execute.

Replays ​

Re-sending the same body with the same key returns the existing action. Nothing is evaluated, approved or dispatched again.

Existing action is…Replay returns
pending_approval or approved202 with the current status
any other state200 with the current status

Differs from the published contract

The OpenAPI contract describes 200 as "replay of an existing action". In the code a replay of an action that is still pending or approved returns 202, like the original submit. Branch on state, not on the HTTP status.

A replay does not auto-execute an approved action: only the first submit of an action allowed within the standing grant dispatches at once. An approved action from an earlier submit needs an explicit resume.

Two submits racing with the same key converge too: the database's unique key lets one insert win, and the loser replays against the winner.

Conflicts ​

Reusing a key for a materially different request is refused, and the existing record is not touched:

http
HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
  "type": "https://docs.sectigoaxiom.com/reference/errors#idempotency_conflict",
  "title": "idempotency conflict",
  "status": 409,
  "code": "idempotency_conflict",
  "detail": "operation key reused with different material parameters; nothing was changed",
  "changed_fields": ["params.amount"],
  "action_id": "act_01kd0m7s2v8x9yz3abcdefghjk",
  "reason_code": "material_change"
}
  • changed_fields names every differing field (tenant, agent, audience, operation, not_after, root_action, resource, params.<name>).
  • action_id is the action the key already belongs to. It is yours; read it.
  • A key already used by another agent in the same tenant and service is also 409 idempotency_conflict ("operation key already used by another actor"), without changed_fields.
  • In the rare race where two different requests with the same key are inserted at the same time, the loser gets 409 idempotency_conflict with action_id but without changed_fields.

Do not "fix" a conflict by editing the request until it matches. A changed amount, beneficiary or tenant is a new intent: it needs a new key and, usually, a fresh approval.

Resume has its own binding check: POST …/resume with parameters or a key that do not reproduce the approved digest is 409 conflict (not idempotency_conflict) with changed_fields and reason_code: material_change.

Choosing keys ​

Choose the key once per intent, before the first attempt, and store it with the intent. Reuse it for every retry of that intent, never for anything else.

ts
import { newOperationKey, deriveOperationKey } from "@sectigo/axiom-sdk";

// A fresh random key for a new intent: "refund-417-6f3c…"
const key = newOperationKey("refund-417");

// A deterministic key from the intent's identifying fields, for runtimes that cannot store a
// random key before submitting. Same parts, same key; any difference, a different key.
const derived = await deriveOperationKey("refund", { ticket: "CS-88121", invoice: "417" });

Keys are 8 to 200 characters. The SDK's submit retries a transport failure (no response) up to twice by default, always with the same key, which is safe for exactly the reasons above.

Keys on the other paths ​

PathWhere the key comes from
REST API, Axiom MCP serveroperation_key in the body or tool arguments.
Hosted HTTP proxyThe Idempotency-Key request header. Required for writes; reads without one get a random gw_read_… key.
Hosted MCP proxy_meta["com.sectigoaxiom/operation_key"] on tools/call if present (8–200 chars). Otherwise a write gets a key derived from agent, service, tool name and the exact arguments (mcp_…), and a read a random mcp_read_… key.
Embedded SDKThe provider passes the agent's operation key in the POST /v1/authorize action.

The derived MCP key means a retried tool call after a lost response converges on the same action. It also means two deliberate, identical tool calls (same arguments) are treated as one; pass an explicit key in _meta if you intend two separate operations. A tool call with a changed amount gets a different derived key, so it becomes a new action rather than a conflict.

On the provider side ​

Axiom forwards the key to the upstream as Idempotency-Key, and inside the signed Axiom-Authorization as operation_key. The provider must persist it atomically with the business write, or use a target with equivalent idempotency and result lookup. A shared key alone cannot guarantee one side effect across independent systems. The SDK's executeOnce does this, and its operation records are keyed by (tenant, operation_key).

Duplicate notifications ​

Wake events (webhooks or a relay) are deduplicated by (action_id, seq). See webhooks and relay.

As the brief requires, every notification causes a status read: the SDK's handleWake reads authoritative status for fresh, duplicate and stale deliveries alike, and the sequence store only tells you whether you have seen the event before. Because the read returns the current record, a duplicate or out-of-order delivery converges on the same state.

Timeouts after dispatch ​

If the provider does not answer after dispatch (network error, timeout, a 5xx, 408 or 429, or an outcome attestation that fails verification), the action becomes outcome_unknown with reason_code: target_timeout. Axiom then asks the provider for the outcome of the original operation key at GET /.well-known/axiom/operations/{operation_key}, and restores succeeded or failed only from a valid signed attestation. Lookups back off exponentially (from about a minute, capped at an hour) for up to 12 attempts; an action still unknown after that stays outcome_unknown for an operator to resolve. The agent never resubmits or resumes again; it tracks the same action ID. See execution and delivery.

Developer preview. Contract version 2026-11-14.