Skip to content

Receipts and verification ​

A receipt links the grant, the approvals, the request and the outcome of one action. Both the customer and the provider can verify it without an Axiom login, identify each signer and see which phases are proven. The field-by-field format is in Receipt and event formats.

When receipts are issued ​

A receipt is issued when an action reaches an outcome: succeeded, failed or outcome_unknown. If reconciliation later establishes the final state, the receipt is reissued with the same receipt ID and the same commitment salts, so earlier redacted exports stay consistent. Actions closed before dispatch (denied, expired, revoked, cancelled) have no receipt; their evidence is the action timeline.

Retention follows the project's plan (14 days on Free; see Plans and limits). Each receipt carries retain_until; the maintenance sweep deletes receipts past it unless they are under legal hold, and only for actions in a final state (succeeded or failed), so evidence of work still being reconciled is kept.

Who signs what ​

Each claim names its signer. Axiom never signs on behalf of someone else.

SignerKey roleSigns
AxiomobservationThe receipt itself (axiom.receipt.v1, typ: axiom-receipt+jws), and its observation of each approval decision.
The provider (receiving app)its outcome keyThe axiom.outcome.v1 attestation of what it committed, embedded in the receipt as a compact JWS.
AxiomgrantThe axiom.dispatch.v1 authorisation sent with the dispatched request (not part of the receipt).

When an external approval system cannot sign the human's decision, the receipt says the decision was verified_via: connector_readback and keeps the native record reference: the observation is Axiom's, and the receipt does not pretend otherwise.

Every signature carries a kid. Axiom's JWKS (/.well-known/jwks.json) keeps retired keys published, annotated axiom:status and axiom:role, so old receipts verify after rotation. Verifying an old signature and establishing a key's current status are separate operations.

Commitments and redaction ​

The receipt does not contain the request fields in the clear. For every canonical field (tenant, agent, audience, operation, resource, not_after, root_action and each params.*) it carries a salted commitment:

text
commitment = "sha-256:" + base64url(SHA-256(salt + "." + JCS(value)))

The salts and values (the disclosures) travel next to the signed receipt in the bundle, not inside it. A redacted export simply leaves some disclosures out:

http
GET /v1/actions/{action_id}/receipt?reveal=operation,params.amount,params.currency

A verifier checks each disclosed value against its signed commitment. A redacted bundle still proves every field it shows; it cannot recompute the request digest, and says so. Salting matters because a low-entropy field such as an amount or currency would be guessable from a plain hash.

Phases ​

The verifier distinguishes four things that are often conflated:

PhaseTrue when
authorizedThe policy decision was allow or require-approval (not deny).
approvedNo approval was needed, or every required approval was approved.
dispatchedThe receipt has a dispatch record (time, claim ID, via proxy/sdk/adapter, which gateway).
destination_confirmedA provider outcome attestation verifies and is bound to this action, operation key and digest.

An outcome the gateway only observed (an unsigned 2xx) is dispatched but not destination_confirmed, and the verifier reports the outcome check as missing, "gateway observed HTTP 201 only".

Verifying ​

The bundle (axiom.receipt.bundle.v1) embeds a snapshot of the public keys that signed it, so it verifies offline:

sh
axiom verify receipt.json                                        # bundle snapshot
axiom verify receipt.json --issuer-jwks https://api.sectigoaxiom.com/.well-known/jwks.json \
  --provider-jwks https://billing.example.com/.well-known/axiom/outcome-keys.json \
  --trusted-issuer https://api.sectigoaxiom.com
ts
import { verifyReceipt } from "@sectigo/axiom-sdk/verify";
const v = await verifyReceipt(bundle, { issuerJwksUrl: "https://api.sectigoaxiom.com/.well-known/jwks.json" });
console.log(v.valid, v.report.phases, v.text);

The checks are: the observation signature(s), the receipt version, the issuer, each disclosure against its commitment, the request digest (when everything is disclosed), each approval's digest and decision, a required approval that is missing, and the provider's outcome attestation. Any fail makes the receipt invalid; missing and warn are reported but do not.

What the bundle snapshot proves

Verifying against the keys embedded in the bundle proves the bundle is internally consistent: anyone can produce a self-consistent bundle with their own keys. To prove that Axiom and the provider signed it, pin or fetch the keys: the issuer JWKS, the provider's published outcome keys, and --trusted-issuer. A key the issuer now marks revoked makes the result not valid.

The two signer roles are kept apart. Bundles carry the provider's outcome keys separately (provider_keys), and the verifier never accepts an outcome attestation from a key that signed the receipt or from a pinned issuer key, so an Axiom key cannot pass as the provider. The attestation must also carry typ: axiom-outcome+jws.

The same verification runs in the browser on the receipt verifier, in the console's receipt page, in the axiom_receipt_verify MCP tool and in any independent implementation built against the published reference fixtures.

Developer preview. Contract version 2026-11-14.