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 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
- Mirror a signed receipt: POST /v1/receipts. Idempotent on
action_hash. - Pull the active policy: GET /v1/policy/pull (scope
policy:read). - Open and poll an approval: POST /v1/approvals, then GET /v1/approvals/{action_hash} until it resolves.
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>, orAuthorization: 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.
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).
POST /v1/receiptsParameters
- 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_hashit 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
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:
| status | Meaning |
|---|---|
appended | A new mirror row was written. HTTP 201. |
duplicate | This action_hash was already mirrored; the existing row is returned. |
quota_exceeded | The 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).
GET /v1/policy/pullReturnsPolicyPullResponse
A PolicyPullResponse with the fields below.
Example
curl https://api.heso.ca/v1/policy/pull \
-H "x-api-key: $HESO_API_KEY"The response shape:
| Field | Type | Description |
|---|---|---|
status | string | policy when a bundle is returned. |
policy_id | string | The deployed policy id. Pin against it to detect a policy change. |
policy_hash | string | The content hash of the bundle. |
toml | string | The 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+).
POST /v1/policyParameters
- 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
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).
POST /v1/approvalsParameters
- 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
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.
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
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).
POST /v1/approvals/{action_hash}/submit-tokenParameters
- 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
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+).
GET /v1/audit/chainReturnsAuditChain
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
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+).
GET /v1/analyticsReturnsStatCounter[]
A StatCounter[] — each { key, label, value, display, spark }. Every number is derived from the org’s mirrored receipts and live approval queue.
Example
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.
| Status | Meaning |
|---|---|
201 | A receipt was mirrored, or an approval was opened. |
401 | Missing or invalid key / session. |
402 | The monthly mirror soft-cap is reached. Your local chain is unaffected. |
403 | The key lacks the scope for this endpoint. |
404 | No such receipt, approval, or supersede target. |
409 | Conflict — a receipt hash/signature mismatch, a chain constraint, or a failed concurrency check. |
422 | A body that fails schema validation (e.g. missing receipt content, bad policy parse). |
429 | Rate 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.