Skip to content

Submit, status and resume ​

The base contract is three operations: submit an action and get its durable ID and state, read its status, and resume an approved action as the original agent. Cancel and receipt complete the set. All five are apps/api/src/routes/actions.ts; the request bodies are the zod schemas exported there and published in the API reference.

The examples use the axiom-headers.ts helper from authentication and the sample project's IDs. Replace them with yours.

sh
export AXIOM_API=https://api.sectigoaxiom.com
export SERVICE=svc_01kd0m5q1w2e3r4t5y6u7i8o9p
export TENANT=ten_01kd0m4z9x8c7v6b5n4m3l2k1j

Discover capabilities ​

GET /v1/capabilities needs no authentication. Read it before choosing how to wait.

sh
curl -sS "$AXIOM_API/v1/capabilities"
json
{
  "api_version": "2026-11-14",
  "issuer": "https://api.sectigoaxiom.com",
  "supported_modes": ["poll", "webhook", "mcp_task"],
  "mcp": { "protocol_versions": ["2025-06-18", "2025-11-25", "2026-07-28"], "tasks_extension": true, "structured_pending_result": true },
  "max_warm_wait_seconds": 25,
  "default_retry_after_seconds": 15,
  "resume_actor": "original_agent",
  "jwks_uri": "https://api.sectigoaxiom.com/.well-known/jwks.json",
  "token_endpoint": "https://api.sectigoaxiom.com/oauth/token",
  "receipt_format": "axiom.receipt.v1",
  "canonical_format": "axiom.action.v1",
  "delegation": { "max_depth": 1 }
}

Submit ​

POST /v1/actions with a token that has actions:submit.

FieldTypeNotes
operation_keystring, 8–200Your idempotency key. Choose it once per intent and reuse it for every retry.
operationstringA protected operation on the service, e.g. billing.refund.create.
servicestringThe protected service ID (svc_…).
tenantstringMust equal your agent's tenant, or 403 forbidden (tenant_mismatch).
resource{ type, id }type must equal the operation's resource type.
paramsobjectOnly the operation's declared material fields. Decimals as strings ("25000").
not_afterRFC 3339, optionalEnd of request validity. Default 24 hours from first submit.
parent_action_idstring, optionalFor a delegated child operation; sets the root action.
intentstring ≤ 500, optionalFree text shown to approvers. Never used for authorisation.
payloadobject, optionalNon-material passthrough added to the upstream body on dispatch. Never stored or approved.
executeboolean, default trueDispatch at once when the action is allowed within the standing grant.

Material fields are validated against the operation's spec: decimal must match -?(0|[1-9]\d*)(\.\d+)? and is normalised ("25000.00" becomes "25000"), currency must be an ISO 4217 code and is upper-cased, integer must be a safe integer, boolean must be a JSON boolean. An undeclared field is 422 validation_failed with issues[].

sh
cat > refund.json <<'EOF'
{
  "operation_key": "refund-417-7Qm2xK9pLr",
  "operation": "billing.refund.create",
  "service": "svc_01kd0m5q1w2e3r4t5y6u7i8o9p",
  "tenant": "ten_01kd0m4z9x8c7v6b5n4m3l2k1j",
  "resource": { "type": "invoice", "id": "417" },
  "params": { "amount": "25000", "currency": "USD", "beneficiary": "acct_acme_ops" },
  "intent": "Customer requested refund for duplicate charge"
}
EOF
curl -sS -K <(npx tsx axiom-headers.ts POST "$AXIOM_API/v1/actions") \
  -X POST "$AXIOM_API/v1/actions" -H 'content-type: application/json' -d @refund.json
ts
const status = await agent.submit({
  operation_key: "refund-417-7Qm2xK9pLr",
  operation: "billing.refund.create",
  service: "svc_01kd0m5q1w2e3r4t5y6u7i8o9p",
  tenant: "ten_01kd0m4z9x8c7v6b5n4m3l2k1j",
  resource: { type: "invoice", id: "417" },
  params: { amount: "25000", currency: "USD", beneficiary: "acct_acme_ops" },
  intent: "Customer requested refund for duplicate charge",
});

A USD 25,000 refund in the sample needs finance approval, so the answer is 202:

json
{
  "action_id": "act_01kd0m7s2v8x9yz3abcdefghjk",
  "root_action_id": "act_01kd0m7s2v8x9yz3abcdefghjk",
  "state": "pending_approval",
  "reason_code": "finance_approval_required",
  "progress": "waiting_on_person",
  "operation": "billing.refund.create",
  "operation_key": "refund-417-7Qm2xK9pLr",
  "tenant": "ten_01kd0m4z9x8c7v6b5n4m3l2k1j",
  "service": "svc_01kd0m5q1w2e3r4t5y6u7i8o9p",
  "grant_id": "grt_01jcz4h0q4a7s5p6m2x9e8r3tb",
  "request_digest": "sha-256:3q2-7wAAAAAh0bWxfZXhhbXBsZV9kaWdlc3RfZm9yX2ZpeHR1cmU",
  "summary": "Billing agent requests USD 25,000 against invoice 417 for customer Acme Corp (sample customer)",
  "status_url": "https://api.sectigoaxiom.com/v1/actions/act_01kd0m7s2v8x9yz3abcdefghjk",
  "resume_url": "https://api.sectigoaxiom.com/v1/actions/act_01kd0m7s2v8x9yz3abcdefghjk/resume",
  "decision_deadline": "2026-11-14T22:00:00.000Z",
  "expires_at": "2026-11-14T22:00:00.000Z",
  "retry_after_seconds": 15,
  "supported_modes": ["poll", "webhook", "mcp_task"],
  "resume_actor": "original_agent",
  "approvals": [{ "required": 1, "received": 0, "role": "finance_approver", "system": "axiom", "next_party": "finance_approver in axiom" }],
  "created_at": "2026-11-14T18:00:00.000Z",
  "updated_at": "2026-11-14T18:00:00.000Z"
}

HTTP status of a submit ​

StatusWhen
202The action is pending_approval or approved (including a replay of such an action).
201A new action was created and closed synchronously: executed (succeeded, failed, outcome_unknown) or denied.
200A replay of an existing action that is no longer pending or approved.
403Refused before any action was created, e.g. tenant_mismatch or a token without actions:submit.
404The operation is not protected on that service.
409idempotency_conflict: the key belongs to a different request. See idempotency.
422validation_failed, with issues[].
402quota_exceeded: the project's plan limit for new actions is reached. Existing actions keep working.

A policy denial is recorded as an action (state denied, 201), so it appears in evidence and the simulator. Read reason_code, for example ceiling_exceeded, no_grant or policy_deny.

Within a standing grant (for example a USD 120 refund) the action enters approved, and with execute: true the gateway immediately claims it for the same agent and dispatches it. The response is then the final state: succeeded with an outcome, failed, or outcome_unknown if the provider did not answer. With execute: false the action stays approved until you resume it. Immediate execution happens only when Axiom's hosted gateway may execute for the service: not for an embedded-SDK (sdk) service, and not after a cutover to a customer-hosted gateway. In those cases the action is returned approved (202) and is executed through the provider or the gateway that holds the executor flag.

Status ​

GET /v1/actions/{action_id}. Only the original agent (same tenant, same agent ID) can read it; anyone else gets 404. An action ID alone never proves permission.

sh
ID=act_01kd0m7s2v8x9yz3abcdefghjk
curl -sS -K <(npx tsx axiom-headers.ts GET "$AXIOM_API/v1/actions/$ID") "$AXIOM_API/v1/actions/$ID"
ts
const s = await agent.status("act_01kd0m7s2v8x9yz3abcdefghjk");

A status read also:

  • closes a record whose deadline has passed (it returns expired from then on);
  • records the time of the read, which admins see as "last agent status read" on the action timeline.

People with evidence access to the project or tenant can read the same document with their console session or an API key with evidence.read.

Fields worth acting on ​

FieldMeaning
state, reason_codeSee the state machine and reason codes.
progressWhile pending: delivery_pending, waiting_on_person, decision_readback_unavailable; awaiting_agent_resume when approved; reconciling when outcome_unknown.
approvals[]required, received, role, system, and next_party while pending (e.g. finance_approver in servicenow, or approval delivery before delivery).
decision_deadlineLast time a verified decision can arrive.
resume_deadlineLast time to resume an approved action. Never later than the grant's expiry or not_after.
expires_atThe deadline that matters now: resume_deadline when approved, else decision_deadline, else not_after.
retry_after_secondsPresent while pending_approval, executing or outcome_unknown. Do not poll faster.
outcomestate, target_ref, recorded_at, result (the provider's attested detail) once recorded.
receipt_urlPresent once a receipt has been issued.
grant_idThe grant that authorised the action; a delegating agent passes it to /v1/grants/{id}/delegate.

Resume ​

POST /v1/actions/{action_id}/resume, only when an authenticated status read says approved. Send the same operation_key, resource and params that were approved, plus an optional non-material payload.

sh
cat > resume.json <<'EOF'
{
  "operation_key": "refund-417-7Qm2xK9pLr",
  "resource": { "type": "invoice", "id": "417" },
  "params": { "amount": "25000", "currency": "USD", "beneficiary": "acct_acme_ops" }
}
EOF
curl -sS -K <(npx tsx axiom-headers.ts POST "$AXIOM_API/v1/actions/$ID/resume") \
  -X POST "$AXIOM_API/v1/actions/$ID/resume" -H 'content-type: application/json' -d @resume.json
ts
const after = await agent.resume(ID, {
  operation_key: "refund-417-7Qm2xK9pLr",
  resource: { type: "invoice", id: "417" },
  params: { amount: "25000", currency: "USD", beneficiary: "acct_acme_ops" },
});

What Axiom does, in order (resumeAction in apps/api/src/services/actions.ts):

  1. Refuses another agent (403 forbidden, wrong_resume_actor), even a delegate in the same tenant.
  2. Expires the action if the resume deadline has passed.
  3. Returns the existing record unchanged if the action is already executing, succeeded, failed or outcome_unknown. A repeated resume never dispatches twice.
  4. Refuses any other state with 409 invalid_state; while pending_approval the problem carries retry_after_seconds: 15.
  5. Recomputes the canonical digest from your resource and params. A different value, or a different operation_key, is 409 conflict with reason_code: material_change and changed_fields. Nothing is changed; a different request needs a new submit and a new approval.
  6. Rechecks current authority: agent status, credential revocation, the grant (and its ancestors), emergency denies, current policy and the eligibility of the approvers who decided. A revoked grant, suspended agent, revoked credential or emergency deny moves the action to revoked; any other failure (for example a new deny rule, or policy_stale when policy now needs an approval that was not given) moves it to denied. The response is 200 with that state.
  7. Checks that this path may execute: an sdk service can only be claimed by its provider, and after a cutover only the customer-hosted gateway holding the executor flag may claim (both 409 invalid_state, below).
  8. Claims execution with one conditional update (approved → executing) and dispatches.

The response is 202 while executing and 200 otherwise (succeeded, failed, outcome_unknown, or the state the recheck produced). If the resume response is lost, read status. Send another resume only if status still says approved.

Embedded-SDK services

If the service's enforcement mode is sdk, the provider executes, not Axiom. POST …/resume on an approved action of such a service returns 409 invalid_state ("repeat the original request to the provider with the same operation key"). Resume by re-sending your original request to the provider; its POST /v1/authorize call claims execution. See embedded SDK.

After a cutover

When a customer-hosted gateway holds the project's executor flag, every Axiom-hosted path (the REST resume, the hosted HTTP and MCP proxies and the Axiom MCP server) refuses with 409 invalid_state ("execution for this project is owned by gateway gwy_…; resume through it"). The action stays approved. See fleet, gateways and cutover.

Cancel ​

POST /v1/actions/{action_id}/cancel (empty body) cancels a pending_approval or approved action; pending approvals are cancelled with it and the reason becomes cancelled_by_requester. Cancelling an already cancelled action returns it again. Any other state is 409 invalid_state.

sh
curl -sS -K <(npx tsx axiom-headers.ts POST "$AXIOM_API/v1/actions/$ID/cancel") \
  -X POST "$AXIOM_API/v1/actions/$ID/cancel" -H 'content-type: application/json' -d '{}'

Receipt ​

GET /v1/actions/{action_id}/receipt returns an axiom.receipt.bundle.v1 once the action has an outcome (succeeded, failed or outcome_unknown); before that it is 404 ("no receipt has been issued for this action yet"). Add ?reveal=operation,params.amount for a redacted export that discloses only the named fields while still proving them.

sh
curl -sS -K <(npx tsx axiom-headers.ts GET "$AXIOM_API/v1/actions/$ID/receipt") \
  "$AXIOM_API/v1/actions/$ID/receipt" -o receipt.json
node packages/sdk/bin/axiom.mjs verify receipt.json
ts
import { verifyReceipt } from "@sectigo/axiom-sdk/verify";
const bundle = await agent.receipt(ID);
const v = await verifyReceipt(bundle, { issuerJwksUrl: "https://api.sectigoaxiom.com/.well-known/jwks.json" });
console.log(v.text);

The receipt format is in receipt and event formats; what each check proves is in receipts and verification.

Through the hosted HTTP proxy ​

The hosted gateway exposes the provider's real routes at https://gw.sectigoaxiom.com/p/{project_id}/http/… (locally http://localhost:8787/gw/p/{project_id}/http/…). The agent calls the route it would call anyway; the gateway turns the call into an action.

  • Only routes mapped to an operation (http_route on the operation) are reachable. Anything else is 403 forbidden with reason_code: operation_not_permitted. There is no pass-through.
  • The DPoP proof's htu is the gateway URL.
  • Writes need an Idempotency-Key header, which becomes the operation key. Reads without one get a random key and still create an allowed action, so every call is evidence.
  • The tenant comes from your token. The resource ID and material fields come from the request as the mapping says (resource_id_from: path:resource_id, body:field, query:name or header:name; params_from: body or query).
sh
GW=https://gw.sectigoaxiom.com/p/prj_01kd0m3a2s1d0f9g8h7j6k5l4z/http
curl -sS -i -K <(npx tsx axiom-headers.ts POST "$GW/v1/invoices/417/refunds") \
  -X POST "$GW/v1/invoices/417/refunds" \
  -H 'content-type: application/json' -H 'Idempotency-Key: refund-417-7Qm2xK9pLr' \
  -d '{"amount":"25000","currency":"USD","beneficiary":"acct_acme_ops"}'
ResultResponse
Allowed in the standing grantForwarded at once with the original body; the upstream's response with Axiom-Action-Id.
Needs approval, still open202 with the status document and Axiom-Action-Id (and Retry-After). Nothing reached the upstream.
Denied or revoked403 problem with reason_code and action_id.
Expired or cancelled409 invalid_state problem.
Already succeeded or failed (replay)200 with the status document and axiom-replay: true. The original upstream response is not stored.
Upstream did not answer504 upstream_timeout with reason_code: target_timeout; the action is outcome_unknown and is reconciled.

To resume through the proxy, the original agent re-sends the same request with the Axiom-Action-Id header. The action must be yours, in this project, for the operation this route maps to, and the request must reproduce the approved digest; a changed amount is 409 conflict and nothing is forwarded.

sh
curl -sS -i -K <(npx tsx axiom-headers.ts POST "$GW/v1/invoices/417/refunds") \
  -X POST "$GW/v1/invoices/417/refunds" \
  -H 'content-type: application/json' -H 'Idempotency-Key: refund-417-7Qm2xK9pLr' \
  -H 'Axiom-Action-Id: act_01kd0m7s2v8x9yz3abcdefghjk' \
  -d '{"amount":"25000","currency":"USD","beneficiary":"acct_acme_ops"}'

The gateway strips your Authorization, DPoP, cookies and hop-by-hop headers, adds the upstream's configured credentials and Axiom's headers (Idempotency-Key, Axiom-Action-Id, Axiom-Root-Action-Id, Axiom-Authorization), and never leaves the service's upstream origin.

Delegation ​

POST /v1/grants/{grant_id}/delegate lets the agent ask for a narrower child grant for another agent in its tenant, bound to one of its actions. See explicit delegation.

json
{
  "child_agent": "agt_01kd0m9r3c4o5n6c7i8l9e0r1a",
  "root_action_id": "act_01kd0m7s2v8x9yz3abcdefghjk",
  "operations": ["billing.invoice.read"],
  "resources": [{ "type": "invoice", "id": "417" }],
  "expires_at": "2026-11-14T20:00:00Z"
}

Developer preview. Contract version 2026-11-14.