Skip to content

First ten minutes ​

Run the launch demonstration against the Sectigo-hosted sample provider, Acme Billing, using synthetic data. You connect the sample agent, run a permitted read, request a USD 25,000 refund, approve it yourself, resume as the original agent and verify the signed receipt.

There are three ways to do it. They all drive the same API, so the action you create in one shows up in the others.

  1. In the console, with a guided stepper. Nothing to install.
  2. With the TypeScript SDK.
  3. With curl, to see the wire format.

Synthetic data

Everything the sample creates is synthetic. The sample customer tenant is called "Acme Corp (sample customer)" and the invoices are fixed fixtures (417 is a paid USD 30,000.00 invoice). Refunds are recorded per Axiom tenant, so many developers can run the sample at once without affecting each other.

What the sample sets up ​

Creating a project with the sample enabled (POST /v1/projects with "sample": true) seeds:

ItemValue
Customer tenant"Acme Corp (sample customer)". You hold tenant_admin and finance_approver in it.
Protected service"Acme Billing (sample)", kind: both, enforcement_mode: proxy, upstream https://sample.sectigoaxiom.com and its /mcp.
Operationsbilling.refund.create (sensitive_write; amount decimal, currency, beneficiary) and billing.invoice.read (read).
Route mappingsPOST /v1/invoices/{resource_id}/refunds and GET /v1/invoices/{resource_id}; MCP tools issue_refund and get_invoice.
Agent"Billing agent", owned by the axiom-quickstart service. It has no credential yet: you bind one.
GrantBoth operations on invoice:*, valid 30 days, delegable, with two rules (below).

The grant's rules are the illustrative policy from the brief:

json
[
  {
    "id": "finance-approval-over-500",
    "description": "Refunds above USD 500 need a finance approver",
    "effect": "require_approval",
    "operations": ["billing.refund.create"],
    "when": [{ "field": "params.amount", "op": "gt", "value": "500" }],
    "reason_code": "finance_approval_required",
    "approval": {
      "approver_role": "finance_approver",
      "count": 1,
      "separation_of_duties": true,
      "decision_ttl_seconds": 14400,
      "resume_ttl_seconds": 3600
    }
  },
  {
    "id": "refund-ceiling",
    "description": "No refund above USD 100,000, even with approval",
    "effect": "deny",
    "hard": true,
    "operations": ["billing.refund.create"],
    "when": [{ "field": "params.amount", "op": "gt", "value": "100000" }],
    "reason_code": "ceiling_exceeded"
  }
]

Separation of duties excludes the agent's owner, its sponsor and the author of the rule. The sample agent is owned by a service, not by you, and grant rules have no policy author, so you can approve in the sample. In a real tenant the person who owns the agent cannot approve its requests.

In the console ​

  1. Sign in at app.sectigoaxiom.com (passkey, email magic link or your organisation's SSO).
  2. Open Projects, create a project and tick the sample option.
  3. Open the project's Quickstart page. It runs eight steps from your browser:
StepWhat happens
1. Generate the sample agent's keyAn ES256 key pair is created with WebCrypto in this tab. The private key never leaves your browser.
2. Bind it with proof of possessionAxiom issues a single-use challenge; the key signs it and the public key is bound to the agent's stable ID.
3. Get a DPoP-bound access tokenThe agent signs a client assertion and a DPoP proof. The short-lived token only works with proofs from this key.
4. Run a permitted invoice readReading invoice 417 is inside the agent's standing grant, so Gateway allows and dispatches it immediately.
5. Request a USD 25,000 refundRefunds above USD 500 need a finance approver. Gateway records the request and returns pending: the refund has not started.
6. Approve it on the approval pageYou hold the finance approver role in the sample tenant. Approving records your decision against the exact request digest; it does not execute the refund.
7. Resume as the original agentThe same agent presents the action ID and the approved fields. Gateway rechecks authority and policy, claims execution once and dispatches to the sample provider.
8. Verify the signed receiptThe receipt links the grant, your approval, the dispatch and the provider's signed outcome. It is verified here, in your browser.

For step 6, open the link the Quickstart shows, or Approvals in the console navigation, which lists approvals waiting on you. The approval page shows the summary ("Billing agent requests USD 25,000 against invoice 417 for customer Acme Corp (sample customer)"), the approver-visible fields, the deadline, the policy rule that triggered it and whether a second approver is required.

The key and token live in the tab's memory for the session, so you can go to the approval page and come back.

With the TypeScript SDK ​

The SDK is @sectigo/axiom-sdk in packages/sdk of the repository. In this developer preview it is consumed from the monorepo (its entry points are TypeScript sources run with tsx); it is not yet published to a registry.

1. Create a key and bind it ​

sh
git clone --recurse-submodules https://github.com/hassard0/sectigo-axiom.git
cd sectigo-axiom && npm install
node packages/sdk/bin/axiom.mjs keygen --agent agt_…   # the sample agent's ID from the console

keygen writes the private JWK to ~/.config/axiom/keys/{kid}.jwk (mode 0600) and prints the public JWK. Binding it needs proof of possession, which the CLI does for you with your console session (you are the sample tenant's admin):

sh
export AXIOM_SESSION_COOKIE='…'   # the axiom_session cookie from your signed-in browser
node packages/sdk/bin/axiom.mjs agent bind agt_…

Alternatively, open the agent in the console (Customer tenants → Agents → Billing agent), choose to bind your own key, run axiom agent sign-challenge on the payload it shows and paste the proof back. See Registering and admitting agents.

2. Run the flow ​

Set the IDs from the console (tenant, service and agent):

sh
export AXIOM_API=https://api.sectigoaxiom.com       # or http://localhost:8787
export AXIOM_AGENT_ID=agt_…
export AXIOM_KEY_FILE=~/.config/axiom/keys/….jwk
export AXIOM_TENANT=ten_…
export AXIOM_SERVICE=svc_…
ts
// quickstart.ts — run with: npx tsx quickstart.ts
import { readFile } from "node:fs/promises";
import { AxiomAgent, newOperationKey, signerFromJwk, verifyReceipt } from "@sectigo/axiom-sdk";

const env = process.env as Record<string, string>;
const agent = new AxiomAgent({
  baseUrl: env.AXIOM_API,
  agentId: env.AXIOM_AGENT_ID,
  signer: await signerFromJwk(JSON.parse(await readFile(env.AXIOM_KEY_FILE, "utf8"))),
});

// A permitted read: allowed within the standing grant and dispatched at once.
const read = await agent.submit({
  operation_key: newOperationKey("read-417"),
  operation: "billing.invoice.read",
  service: env.AXIOM_SERVICE,
  tenant: env.AXIOM_TENANT,
  resource: { type: "invoice", id: "417" },
  params: {},
});
// succeeded gateway_observed_success. The sample does not sign reads, so the gateway only observed
// HTTP 200; the receipt does not mark this outcome destination-confirmed.
console.log(read.state, read.reason_code);

// The protected write. Choose the operation key once per intent and keep it.
const refund = {
  operation_key: newOperationKey("refund-417"),
  operation: "billing.refund.create",
  service: env.AXIOM_SERVICE,
  tenant: env.AXIOM_TENANT,
  resource: { type: "invoice", id: "417" },
  params: { amount: "25000", currency: "USD", beneficiary: "Acme Corp refund account (sample)" },
  intent: "Refund the duplicate charge on invoice 417",
};
const pending = await agent.submit(refund);
console.log(pending.state, pending.approvals?.[0]?.next_party); // pending_approval, "finance_approver in axiom"
console.log(`Approve it at https://app.sectigoaxiom.com/app/approvals, then press Enter.`);
await new Promise((r) => process.stdin.once("data", r));

// Resume as the original agent with the same approved fields, then track to a confirmed outcome.
const result = await agent.continue(agent.pendingRecord(pending, refund), { budgetSeconds: 25 });
console.log(result.kind, result.status.state, result.status.outcome?.target_ref);

// Download and verify the receipt offline.
const bundle = await agent.receipt(pending.action_id);
const v = await verifyReceipt(bundle);
console.log(v.text);

agent.run(refund, { budgetSeconds }) does submit, bounded wait, resume and tracking in one call; when the budget ends while a person still has to decide, it returns { kind: "waiting", record } for you to continue later. See Waiting for a decision.

With curl ​

Every API call needs an access token and a fresh DPoP proof bound to the method, URL and token. Authentication defines a small helper, axiom-headers.ts, that prints both headers in curl's config format. With the environment variables above:

Discover capabilities ​

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 the refund ​

sh
cat > refund.json <<EOF
{
  "operation_key": "refund-417-$(openssl rand -hex 8)",
  "operation": "billing.refund.create",
  "service": "$AXIOM_SERVICE",
  "tenant": "$AXIOM_TENANT",
  "resource": { "type": "invoice", "id": "417" },
  "params": { "amount": "25000", "currency": "USD", "beneficiary": "Acme Corp refund account (sample)" },
  "intent": "Refund the duplicate charge on invoice 417"
}
EOF

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

The answer is 202 Accepted with the action's status document:

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-9f2c1a7e5b3d4c60",
  "tenant": "ten_…",
  "service": "svc_…",
  "grant_id": "grt_…",
  "request_digest": "sha-256:…",
  "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"
}

progress starts as delivery_pending and becomes waiting_on_person once the approval request has been delivered (here, an email to each eligible approver with a link to the approval page). Nothing has been sent to Acme Billing.

Approve, then read status ​

Approve on the approval page. Then read status as the agent:

sh
ACTION=act_01kd0m7s2v8x9yz3abcdefghjk
curl -sS "$AXIOM_API/v1/actions/$ACTION" -K <(npx tsx axiom-headers.ts GET "$AXIOM_API/v1/actions/$ACTION")

The action is now approved with reason_code: "awaiting_agent_resume", progress: "awaiting_agent_resume" and a resume_deadline.

Resume as the original agent ​

Send the same operation key, resource and params that were approved:

sh
jq '{operation_key, resource, params}' refund.json > resume.json
curl -sS -X POST "$AXIOM_API/v1/actions/$ACTION/resume" \
  -K <(npx tsx axiom-headers.ts POST "$AXIOM_API/v1/actions/$ACTION/resume") \
  -H 'content-type: application/json' -d @resume.json

Gateway rechecks the agent, credential, grant, emergency denies, current policy and approver eligibility, claims execution with a single conditional update and dispatches POST https://sample.sectigoaxiom.com/v1/invoices/417/refunds with Idempotency-Key, Axiom-Action-Id and a signed Axiom-Authorization. Acme Billing commits the refund with the operation key and answers with an axiom-outcome attestation, so the response shows state: "succeeded", reason_code: "target_confirmed", an outcome.target_ref such as re_01kd… and a receipt_url.

Changing any approved field returns 409 conflict with reason_code: "material_change" and changed_fields, and nothing is dispatched.

Download and verify the receipt ​

sh
curl -sS "$AXIOM_API/v1/actions/$ACTION/receipt" \
  -K <(npx tsx axiom-headers.ts GET "$AXIOM_API/v1/actions/$ACTION/receipt") > receipt.json
node packages/sdk/bin/axiom.mjs verify receipt.json --issuer-jwks "$AXIOM_API/.well-known/jwks.json"

The verifier reports each signer (Axiom's observation key and Acme Billing's outcome key), the disclosed fields and four phases: authorized, approved, dispatched and destination confirmed. You can also drop the file on the receipt verifier.

Try the edges ​

TryExpected result
Refund 120 instead of 25000Allowed within the standing grant and dispatched at once: 201 with succeeded.
Refund 150000201 with state: "denied", reason_code: "ceiling_exceeded". The hard ceiling wins over any approval.
Re-send refund.json unchangedThe existing action comes back (no second action, no second approval request).
Re-send it with "amount": "26000" and the same key409 idempotency_conflict, changed_fields: ["params.amount"]. The original action is unchanged.
Resume before approving409 invalid_state with retry_after_seconds: 15.
Do nothing for four hoursThe decision deadline passes and the action becomes expired (approval_expired). A late approval cannot revive it.

Next ​

Developer preview. Contract version 2026-11-14.