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.
export AXIOM_API=https://api.sectigoaxiom.com
export SERVICE=svc_01kd0m5q1w2e3r4t5y6u7i8o9p
export TENANT=ten_01kd0m4z9x8c7v6b5n4m3l2k1jDiscover capabilities
GET /v1/capabilities needs no authentication. Read it before choosing how to wait.
curl -sS "$AXIOM_API/v1/capabilities"{
"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.
| Field | Type | Notes |
|---|---|---|
operation_key | string, 8–200 | Your idempotency key. Choose it once per intent and reuse it for every retry. |
operation | string | A protected operation on the service, e.g. billing.refund.create. |
service | string | The protected service ID (svc_…). |
tenant | string | Must equal your agent's tenant, or 403 forbidden (tenant_mismatch). |
resource | { type, id } | type must equal the operation's resource type. |
params | object | Only the operation's declared material fields. Decimals as strings ("25000"). |
not_after | RFC 3339, optional | End of request validity. Default 24 hours from first submit. |
parent_action_id | string, optional | For a delegated child operation; sets the root action. |
intent | string ≤ 500, optional | Free text shown to approvers. Never used for authorisation. |
payload | object, optional | Non-material passthrough added to the upstream body on dispatch. Never stored or approved. |
execute | boolean, default true | Dispatch 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[].
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.jsonconst 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:
{
"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
| Status | When |
|---|---|
202 | The action is pending_approval or approved (including a replay of such an action). |
201 | A new action was created and closed synchronously: executed (succeeded, failed, outcome_unknown) or denied. |
200 | A replay of an existing action that is no longer pending or approved. |
403 | Refused before any action was created, e.g. tenant_mismatch or a token without actions:submit. |
404 | The operation is not protected on that service. |
409 | idempotency_conflict: the key belongs to a different request. See idempotency. |
422 | validation_failed, with issues[]. |
402 | quota_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.
ID=act_01kd0m7s2v8x9yz3abcdefghjk
curl -sS -K <(npx tsx axiom-headers.ts GET "$AXIOM_API/v1/actions/$ID") "$AXIOM_API/v1/actions/$ID"const s = await agent.status("act_01kd0m7s2v8x9yz3abcdefghjk");A status read also:
- closes a record whose deadline has passed (it returns
expiredfrom 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
| Field | Meaning |
|---|---|
state, reason_code | See the state machine and reason codes. |
progress | While 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_deadline | Last time a verified decision can arrive. |
resume_deadline | Last time to resume an approved action. Never later than the grant's expiry or not_after. |
expires_at | The deadline that matters now: resume_deadline when approved, else decision_deadline, else not_after. |
retry_after_seconds | Present while pending_approval, executing or outcome_unknown. Do not poll faster. |
outcome | state, target_ref, recorded_at, result (the provider's attested detail) once recorded. |
receipt_url | Present once a receipt has been issued. |
grant_id | The 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.
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.jsonconst 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):
- Refuses another agent (
403 forbidden,wrong_resume_actor), even a delegate in the same tenant. - Expires the action if the resume deadline has passed.
- Returns the existing record unchanged if the action is already
executing,succeeded,failedoroutcome_unknown. A repeated resume never dispatches twice. - Refuses any other state with
409 invalid_state; whilepending_approvalthe problem carriesretry_after_seconds: 15. - Recomputes the canonical digest from your
resourceandparams. A different value, or a differentoperation_key, is409 conflictwithreason_code: material_changeandchanged_fields. Nothing is changed; a different request needs a new submit and a new approval. - 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, orpolicy_stalewhen policy now needs an approval that was not given) moves it todenied. The response is200with that state. - Checks that this path may execute: an
sdkservice can only be claimed by its provider, and after a cutover only the customer-hosted gateway holding the executor flag may claim (both409 invalid_state, below). - 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.
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.
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.jsonimport { 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_routeon the operation) are reachable. Anything else is403 forbiddenwithreason_code: operation_not_permitted. There is no pass-through. - The DPoP proof's
htuis the gateway URL. - Writes need an
Idempotency-Keyheader, 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:nameorheader:name;params_from:bodyorquery).
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"}'| Result | Response |
|---|---|
| Allowed in the standing grant | Forwarded at once with the original body; the upstream's response with Axiom-Action-Id. |
| Needs approval, still open | 202 with the status document and Axiom-Action-Id (and Retry-After). Nothing reached the upstream. |
| Denied or revoked | 403 problem with reason_code and action_id. |
| Expired or cancelled | 409 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 answer | 504 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.
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.
{
"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"
}