Cloud API /API reference

API reference

The cloud API is optional. Gating, signing, and verification run locally and need no key. You call this API to mirror receipts for audit, pull and update policy, and run human approvals.

HESO works fully offline. The SDK gates an action against policy, signs the Action Receipt, and a verifier checks it anywhere — none of that touches this API. Reach for the cloud when you want a central mirror of receipts, a policy you push from a dashboard, or approvals routed to a human.

Every request authenticates with an API key. The org (tenant) is resolved from the key, so no path carries a team id. See Authentication for where keys and scopes come from.

The cloud is a mirror, not the source of truth

The cloud receipt store is a non-authoritative mirror. Your local BLAKE3-chained ledger is the source of truth. An accepted receipt proves you authorized an action under your policy. It does not prove the action succeeded downstream, and the cloud holds no signing key at any level.

Common tasks

Base URL and authentication

The base URL is https://api.heso.ca (configurable per deployment). Authenticate with one of two headers:

  • x-api-key: <key>, or
  • Authorization: Bearer <key>

Keys carry scopes set in the dashboard (for example receipts:write, policy:read, approvals:write). A key missing the scope for an endpoint gets a 403. Request and response bodies are JSON.

auth.shbash
curl https://api.heso.ca/v1/policy/pull \
  -H "x-api-key: $HESO_API_KEY"

# Bearer form is equivalent:
curl https://api.heso.ca/v1/policy/pull \
  -H "Authorization: Bearer $HESO_API_KEY"

Two planes share this host. The SDK plane (the endpoints your agent calls) authenticates with an API key. The dashboard plane (audit chain, analytics, policy updates) authenticates with a session JWT from the console, not an API key. Each endpoint below notes which it uses.

Receipts

Push a signed receipt to the mirror. The server re-verifies every receipt before storing it: it recomputes the BLAKE3 action_hash, checks the Ed25519 signatures, and re-derives the trust level — the same verify orderthe SDKs and browser run. A tampered or malformed body is rejected; acceptance never depends on the client’s word.

POST /v1/receiptsREST

Mirror one signed receipt. Idempotent on action_hash — a re-push of the same receipt returns the existing row, never a second one. Scope receipts:write (SDK plane).

bash
POST /v1/receipts

Parameters

receiptActionReceiptrequired
The full signed receipt, wrapped in a { receipt } body. See the receipt schema for every field.
supersedes_action_hashstring?
Set only when finalizing an approval: a freshly assembled L1 supersedes the suspended L0 entry’s action_hash it was built from.

ReturnsReceiptPushResponse

{ status, entry_hash, seq }. entry_hashechoes the receipt’s own action_hash; seqis the mirror’s per-org append position.

Example

post-receipt.shbash
curl https://api.heso.ca/v1/receipts \
  -H "x-api-key: $HESO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "receipt": { /* full signed ActionReceipt */ } }'

status is one of:

statusMeaning
appendedA new mirror row was written. HTTP 201.
duplicateThis action_hash was already mirrored; the existing row is returned.
quota_exceededThe monthly mirror soft-cap is reached (HTTP 402). Your local chain is never refused — see Rate limits.

There is no batch route; the SDK’s pushReceipts loops over this endpoint in order. A receipt that fails re-verification returns 409 (hash or signature mismatch) or 422 (missing or non-object content).

Policy

Pull the policy your agent enforces, or push a new one from the dashboard. The bundle is the heso.toml the engine loads, plus an id and a content hash you can pin against. Floors and parsing are checked at deploy time, so any bundle you pull has already passed those checks.

GET /v1/policy/pullREST

Fetch the org’s active policy bundle. Scope policy:read (SDK plane).

bash
GET /v1/policy/pull

ReturnsPolicyPullResponse

A PolicyPullResponse with the fields below.

Example

get-policy.shbash
curl https://api.heso.ca/v1/policy/pull \
  -H "x-api-key: $HESO_API_KEY"

The response shape:

FieldTypeDescription
statusstringpolicy when a bundle is returned.
policy_idstringThe deployed policy id. Pin against it to detect a policy change.
policy_hashstringThe content hash of the bundle.
tomlstringThe policy TOML the engine loads. An empty string is the fail-safe deny pin — the engine routes every action to a human when the org has no resolvable policy.

POST /v1/policyREST

Deploy a new policy and activate it. Validated (parse + dangerous-lane floor) before any write. Dashboard plane (session JWT, manager+).

bash
POST /v1/policy

Parameters

tomlstringrequired
The full policy TOML to deploy. See Policy for the rule format.
expected_policy_hashstring?
Optimistic concurrency: if set and it no longer matches the active pin, the deploy is refused 409 (another manager moved the policy first).

ReturnsPolicyPullResponse

A PolicyPullResponse for the newly activated policy (same shape as the pull above).

Example

deploy-policy.shbash
curl https://api.heso.ca/v1/policy \
  -H "Authorization: Bearer $SESSION_JWT" \
  -H "Content-Type: application/json" \
  -d '{ "toml": "[[rule]]\nid = \"...\"\n..." }'

A bad parse returns 422; a floor bypass attempt returns 409. Same TOML deploys to the same id and hash, so a re-deploy is idempotent.

Approvals

When policy routes an action to a human, you open an approval keyed by that action’s action_hash, poll it until it resolves, then finalize the L1 in-core. The approver signs with their own device-held key — the cloud never signs. See Human approval for the full flow.

POST /v1/approvalsREST

Open (or idempotently re-open) a pending approval for a suspended action. Scope approvals:write (SDK plane).

bash
POST /v1/approvals

Parameters

action_hashstringrequired
The action_hash of the suspended action this approval gates.
rule_idstring?
The policy rule that gated the action. Binds the scope the approver token must cover.
thresholdnumber?= 1
The k in a k-of-n quorum. Defaults to a single approver.
workflow, accountstring?
Optional display context carried into the approval queue.

ReturnsApprovalView

An ApprovalView: { approval_id, action_hash, status, threshold, approved_count, resolved_at }. status starts pending. HTTP 201.

Example

open-approval.shbash
curl https://api.heso.ca/v1/approvals \
  -H "x-api-key: $HESO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action_hash": "<hex>", "rule_id": "pay.large", "threshold": 1 }'

One approval per (org, action_hash); re-opening the same action returns the existing approval.

GET /v1/approvals/{action_hash}REST

Poll the current state of an approval, keyed by the action it gates.

bash
GET /v1/approvals/{action_hash}

Parameters

action_hashstringrequired
Path parameter. The action_hash of the suspended action.

ReturnsApprovalView

An ApprovalView. status is pending, approved, or rejected.

Example

poll-approval.shbash
curl https://api.heso.ca/v1/approvals/<action_hash> \
  -H "x-api-key: $HESO_API_KEY"

The TypeScript SDK’s pollApproval and waitForApproval wrap this — reach for them instead of writing your own loop. When threshold is greater than one the approval is a k-of-n quorum: approved_count tracks how many distinct approvers have signed, and the assembled receipt is still L1, not a higher level.

POST /v1/approvals/{action_hash}/submit-tokenREST

Submit a human-signed approval token to record one verified vote. Scope approvals:write (SDK plane).

bash
POST /v1/approvals/{action_hash}/submit-token

Parameters

action_hashstringrequired
Path parameter. The action being approved.
token_b64stringrequired
The approval-token wire bytes, base64.
action_contentActionContentrequired
The receipt content the token signs over; the cloud re-derives the canonical bytes from it.
decisionstring?= approved
The human's declared intent: "approved" or "rejected".

ReturnsApprovalView

The updated ApprovalView. The cloud verifies the token against the org’s registered approver keys and records it as one vote — on a k-of-n quorum each of the threshold approvers submits their own token.

Example

submit-token.shbash
curl https://api.heso.ca/v1/approvals/<action_hash>/submit-token \
  -H "x-api-key: $HESO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "token_b64": "…", "action_content": { /* receipt.content */ }, "decision": "approved" }'

Audit and analytics

Two read endpoints for the dashboard. Both authenticate with a session JWT from the console, not an API key, and are scoped to your org under row-level security.

GET /v1/audit/chainREST

The org's tamper-evident receipt chain. Dashboard plane (session JWT, auditor+).

bash
GET /v1/audit/chain

ReturnsAuditChain

An AuditChain: { head, size, entries }. Each entry links to the previous via a domain-separated BLAKE3 hash (chainHash), byte-identical to the browser verifier. Entries are newest-first; head is the last link.

Example

audit-chain.shbash
curl https://api.heso.ca/v1/audit/chain \
  -H "Authorization: Bearer $SESSION_JWT"

Flipping any leaf diverges every downstream link, so an offline re-derivation in the console is tamper-evident against this server-built chain. See Verification.

GET /v1/analyticsREST

Headline governance counts for the dashboard. Dashboard plane (session JWT, viewer+).

bash
GET /v1/analytics

ReturnsStatCounter[]

A StatCounter[] — each { key, label, value, display, spark }. Every number is derived from the org’s mirrored receipts and live approval queue.

Example

analytics.shbash
curl https://api.heso.ca/v1/analytics \
  -H "Authorization: Bearer $SESSION_JWT"

Errors and limits

A non-2xx response carries a JSON error body ({ "detail": … }). The TypeScript client surfaces this as a thrown Error whose message includes the HTTP status, statusText, and response body.

StatusMeaning
201A receipt was mirrored, or an approval was opened.
401Missing or invalid key / session.
402The monthly mirror soft-cap is reached. Your local chain is unaffected.
403The key lacks the scope for this endpoint.
404No such receipt, approval, or supersede target.
409Conflict — a receipt hash/signature mismatch, a chain constraint, or a failed concurrency check.
422A body that fails schema validation (e.g. missing receipt content, bad policy parse).
429Rate limited. Wait for the Retry-After header, then retry — see Rate limits.

Rate limits are per-org per-plan (free / pro / team / custom); over the rate the API returns 429. The full headers and back-off guidance live on Rate limits.

Next steps