Receipt and event formats
Every signed Axiom document is a JWS (RFC 7515) with alg ES256 or EdDSA, a kid and a typ. Payloads are canonicalised with the JSON Canonicalization Scheme (RFC 8785) before signing or hashing. Every digest field has the form sha-256: followed by the base64url (unpadded) SHA-256.
| Format | typ | Signed by (key role) | Where it appears |
|---|---|---|---|
axiom.action.v1 | — (not signed; hashed) | — | request_digest, approvals, dispatch claims |
axiom.dispatch.v1 | axiom-dispatch+jws | Axiom grant key | Axiom-Authorization header to the upstream |
axiom.outcome.v1 | axiom-outcome+jws | Provider's outcome key | axiom-outcome response header, POST /v1/actions/:id/outcome, receipt |
axiom.receipt.v1 | axiom-receipt+jws | Axiom observation key | Receipt bundle |
axiom.receipt.bundle.v1 | — (container) | — | GET /v1/actions/:id/receipt, exports |
axiom.event.v1 | axiom-event+jws | Axiom event key | Webhook and relay deliveries |
axiom.observation.v1 | axiom-observation+jws | Axiom observation key | Stored with each approval decision |
Axiom's public keys, current and historical, are published at https://api.sectigoaxiom.com/.well-known/jwks.json with axiom:role (identity, grant, observation, policy, event) and axiom:status (active, retired, revoked). Retired keys stay published so older documents keep verifying.
axiom.action.v1
The canonical action: the versioned representation of exactly what the person approved and what the gateway authorised (packages/core/src/canonical.ts). request_digest is sha-256(JCS(canonical action)). Any change to a field changes the digest, which invalidates an existing approval.
| Field | Type | Meaning |
|---|---|---|
v | "axiom.action.v1" | Version |
tenant | string | Tenant ID, taken from the authenticated agent, never from the request body |
agent | string | Stable agent ID |
audience | string | Protected service ID |
operation | string | Operation name, e.g. billing.refund.create |
resource | { type, id } | The business resource, e.g. { "type": "invoice", "id": "417" } |
params | object | Material fields only, normalised: decimals as canonical strings ("25000.00" becomes "25000"), currencies upper-case, integers as numbers |
not_after | RFC 3339 | End of the request's validity (default 24 hours after submit; reused on replay) |
root_action | string, optional | Root action ID for a delegated child operation |
{
"v": "axiom.action.v1",
"tenant": "ten_01kd0m4z9x8c7v6b5n4m3l2k1j",
"agent": "agt_01kd0m6a1b2c3d4e5f6g7h8j9k",
"audience": "svc_01kd0m5q1w2e3r4t5y6u7i8o9p",
"operation": "billing.refund.create",
"resource": { "type": "invoice", "id": "417" },
"params": { "amount": "25000", "beneficiary": "acct_acme_ops", "currency": "USD" },
"not_after": "2026-11-15T18:00:00.000Z"
}axiom.dispatch.v1
Forwarded by the gateway to the upstream in the Axiom-Authorization header (and returned to an embedded-SDK provider as claim.authorization from POST /v1/authorize). It states exactly what was authorised, so the application can verify trusted context at its boundary. Lifetime: 120 seconds. The same token authenticates Axiom's reconciliation lookup.
| Claim | Type | Meaning |
|---|---|---|
v | "axiom.dispatch.v1" | Version |
iss | string | Axiom issuer, e.g. https://api.sectigoaxiom.com |
aud | string | Origin of the upstream the request is addressed to |
action_id, root_action_id | string | Action and root action |
operation_key | string | Operation key; also sent as Idempotency-Key |
operation, tenant, agent | string | What, for whom, by whom |
resource | { type, id } | Authorised resource |
request_digest | string | Digest of canonical |
canonical | axiom.action.v1 | The approved canonical request; recompute its digest and compare its params with the body received |
claim_id | string | The single execution claim |
iat, exp | number | Issue and expiry (seconds) |
Headers sent with it: Idempotency-Key, Axiom-Action-Id, Axiom-Root-Action-Id. The SDK checks all of this in AxiomProvider.verifyDispatch (Embedded SDK provider guide).
axiom.outcome.v1
The provider's attestation of what its system of record committed. Only a valid attestation, signed by an outcome key registered on the service (outcome_keys) and bound to the action, makes a receipt "destination confirmed".
| Field | Type | Meaning |
|---|---|---|
v | "axiom.outcome.v1" | Version |
action_id | string | Must equal the action |
operation_key | string | Must equal the action's operation key |
request_digest | string | Must equal the action's request digest |
state | succeeded | failed | Committed result. failed means the business rules rejected it and nothing was written |
target_ref | string, optional | Provider's record ID, e.g. the refund ID |
recorded_at | RFC 3339 | When the provider recorded it |
detail | object, optional | Provider-chosen result fields, returned to the agent as outcome.result |
axiom.receipt.v1
The receipt claims (ReceiptClaims in packages/core/src/receipts.ts), signed as a JWS JSON General Serialization whose payload is the JCS of the claims. Each signature's protected header carries typ: axiom-receipt+jws, role: observation and kid. A receipt is issued when an action reaches an outcome (succeeded, failed or outcome_unknown) and reissued, with the same ID and salts, if reconciliation later establishes the final state.
| Field | Type | Meaning |
|---|---|---|
v | "axiom.receipt.v1" | Version |
receipt_id | string | rcp_… |
issuer, issued_at | string | Signer and time of this issue |
action_id, root_action_id, parent_action_id? | string | Action and delegation chain |
operation_key | string | Operation key |
tenant, project, audience, operation | string | Context |
resource | { type, id } | Resource |
agent | object | id, credential: { kind, thumbprint, issuer? }, operator?, publisher?, sponsor? |
request_digest | string | Digest of the canonical action |
commitments | object | Per canonical field, sha-256(salt + "." + JCS(value)). Field names: tenant, agent, audience, operation, resource, not_after, root_action, and params.<name> |
grant | object | id, parent?, issued_by, expires_at |
policy | object | versions: { id, version, layer }[], decision: allow | require_approval, reason_code |
approvals | array | Per decision: approval_id, approver: { id, display? }, role, decision, decided_at, digest, source: { system, native_ref?, verified_via: axiom_session | connector_readback | signed_callback } |
dispatch | object, optional | at, claim_id, via: proxy | sdk | adapter, gateway |
outcome | object, optional | state: succeeded | failed | outcome_unknown, recorded_at, target_ref?, attestation? (the provider's compact axiom.outcome.v1), observed?: { http_status?, observed_at } when only the gateway saw a response |
phases | object | authorized, approved (allowed within the grant, or every required approval approved), dispatched, destination_confirmed (booleans as recorded by Axiom; a verifier recomputes dispatched and destination_confirmed) |
When an external approval system cannot sign the human's decision, Axiom signs its own verified observation (verified_via: connector_readback) and keeps the native reference, so the receipt names the actual signer.
axiom.receipt.bundle.v1
What GET /v1/actions/:id/receipt (and the SDK, CLI and exports) return. It verifies offline.
| Field | Type | Meaning |
|---|---|---|
type | "axiom.receipt.bundle.v1" | Container type |
receipt | General JWS | { payload, signatures: [{ protected, signature }] } over axiom.receipt.v1 |
disclosures | object | Field name → { salt, value }. A redacted export (?reveal=operation,params.amount) contains only the named fields; the commitments still prove what is shown |
keys | JWK[] | Snapshot of Axiom's public keys (and, for older verifiers, the provider keys) at export time |
provider_keys | JWK[] | The service's registered provider outcome keys at export time. Verifiers take outcome keys from here |
status | object, optional | { checked_at, keys: { kid: active | retired | revoked }, source }: key status at export |
Pin keys for trust
The embedded keys let anyone check a bundle's internal consistency without network access, but a bundle could carry any keys. For a trust decision, verify with Axiom's published JWKS and the provider's published outcome keys (axiom verify --issuer-jwks … --provider-jwks …), or pinned keys.
How the reference verifier (verifyReceiptBundle in packages/core/src/receipts.ts) keeps the two signer roles apart:
- The receipt signatures are checked against the pinned issuer keys, or the
keyssnapshot. - Outcome attestations are checked against the pinned provider keys, else
provider_keys, elsekeys, after removing every key ID that signed the receipt and every pinned issuer key. An Axiom key therefore can never pass as the provider's outcome signer. - The attestation's protected header must have
typ: axiom-outcome+jws, and itsaction_id,request_digestandoperation_keymust equal the receipt's.
axiom.event.v1
The data-minimised wake event delivered to webhooks and relays, as a compact JWS (content-type: application/jose, header Axiom-Event-Id). It is a hint to read status: it carries no state an agent may act on and cannot grant authority.
| Field | Type | Meaning |
|---|---|---|
v | "axiom.event.v1" | Version |
event_id | string | Unique per delivery target and change; receivers deduplicate on it |
type | action.changed (also defined: agent.suspended, grant.revoked, policy.updated) | Event type. The current release delivers action.changed |
action_id | string, optional | Action that changed |
tenant | string | Tenant |
occurred_at | RFC 3339 | When the change was recorded |
seq | number | Per-action sequence at the time of the change; drop events with a seq at or below the last processed |
Other signed documents
- Policy versions are signed on publish with the
policykey (typ: axiom-policy+jws) over{ policy_id, version, digest, scope, signed_at }. - Gateway bundles (
GET /v1/gateways/:gid/bundle, heartbeat responses) are compact JWS from thepolicykey withissued_at,fresh_until(300 seconds),policy_version,executor,policies[],trustandemergency_denies[]. - Incident bundles are signed with the
eventkey and omit request fields. - SIEM events are unsigned JSON over an authenticated channel:
{ id, seq, occurred_at, org_id, tenant_id, project_id, type, action_id, actor: { type, id }, policy_version, evidence_ref, outcome, data }. See SIEM delivery.