Start free

Quickstart: TypeScript

Verify receipts and enforce a minimum trust level from Node with @hesohq/sdk, then push receipts to the cloud outbox. The SDK is a thin wrapper over @hesohq/core, so the verdict you get here is byte-identical to the one a reviewer’s browser computes.

A full TypeScript walkthrough. In Node you verify receipts and talk to the cloud control plane; capturing and gating your own agent with decorators is the job of the Python SDK. Everything below runs in-process — no subprocess, and no re-implementing the crypto in JavaScript.

Install

Add the SDK to a Node project (Node 18 or later). @hesohq/sdk pulls in the native verifier @hesohq/core as a dependency, so verification works with no extra setup.

bash
pnpm add @hesohq/sdk

Configure the cloud client

Call configure once at startup, before any cloud call, with your API key and endpoint. This is needed only for the cloud client — pushReceipt, openApproval, and friends. Local verification (gate, assertGate) needs no configuration and no network.

heso.ts
import { configure } from "@hesohq/sdk" configure(process.env.HESO_API_KEY!, "https://api.your-endpoint")
Keep keys in the environment

Read the API key from process.env. See Authentication for the header format, the environment variables, and how the SDK and CLI find your credentials.

Verify a receipt

gate verifies receipt bytes locally and returns a structured result. Pass a JSON string or a Buffer; the second argument is the minimum trust level to accept, defaulting to "L0".

verify.ts
import { gate } from "@hesohq/sdk"import { readFileSync } from "node:fs" const receiptJson = readFileSync("receipt.json", "utf8") const r = gate(receiptJson)console.log(r.allowed)     // trueconsole.log(r.verdict)     // "valid"console.log(r.trustLevel)  // "L0"

It returns a GateResult, which is small on purpose:

allowedboolean
true when the receipt verifies and its trust level meets the minimum you asked for.
trustLevelTrustLevel | null
The trust level re-derived from which signatures pass — "L0" (operator-signed) or "L1" (operator plus human co-signature). null when verification fails before a level can be established.
verdictstring
The verification outcome, e.g. "valid". On failure it names the gate that failed, such as "hash_mismatch" or "invalid_signature". See Verdicts for the full list.

Trust is never trusted from the wire — it is re-derived on every verify from the signatures that actually pass. A receipt that claims "L1" but carries only an operator signature fails with the trust_mismatch verdict. Read Offline verification for the full seven-gate order.

Gate by trust level

When you want to halt on a bad receipt rather than branch on a boolean, use assertGate. It verifies and checks the minimum trust level, and throws if either fails. Asking for "L1" means the action must be valid and human co-signed before your code continues.

apply.ts
import { assertGate } from "@hesohq/sdk" // Throws unless the receipt verifies AND a human co-signed it (L1).assertGate(receiptJson, "L1") // Past this line, the action was authorized and human-approved.applyTransfer()

To branch on what policy decided rather than on cryptographic validity, use isDecisionAllowed. It takes a parsed ActionReceipt and the set of decision paths you accept, and returns a boolean.

decision.ts
import { isDecisionAllowed } from "@hesohq/sdk"import type { ActionReceipt } from "@hesohq/sdk" const receipt: ActionReceipt = JSON.parse(receiptJson) // Accept actions that policy let through or redacted; reject blocks.if (isDecisionAllowed(receipt, ["allow", "redact"])) {  proceed()}

The four decision paths it can match are:

  • allow — policy let the action through.
  • block — policy stopped the action.
  • redact — policy stripped sensitive fields, then let it through.
  • require_approval — policy routed the action to a human.

See Policy & decisions for how those paths are chosen and Trust levels for the L0/L1 distinction.

Wrap a client

wrap returns a Proxy around a client object. After each method call it looks for a __heso_receipt field on the result and gates that receipt against your minTrust. One thing to be clear about: it only gates clients that attach a receipt to their response. A method that returns no __heso_receipt is passed through unguarded.

wrap.ts
import { wrap, pushReceipt } from "@hesohq/sdk" const guarded = wrap(agentClient, {  minTrust: "L1",  onReceipt: async (method, receiptJson) => {    await pushReceipt(JSON.parse(receiptJson))  },  onGateFail: (method, verdict) => {    console.error(`${method} failed gating: ${verdict}`)    return false // re-throw the gate failure  },}) // Every result that carries a __heso_receipt field is gated automatically.const out = await guarded.transfer({ amountUsd: 4200 })

The WrapOptions hooks let you set the bar, persist receipts, and decide what happens on a failed gate:

minTrustTrustLevel
The minimum trust level every attached receipt must meet, e.g. "L1" to require a human co-signature.
onReceipt(method, receiptJson) => void | Promise<void>
Called with the method name and the receipt JSON for each gated result — a natural place to pushReceipt into the cloud outbox.
onGateFail(method, verdict) => boolean | Promise<boolean>
Called when a receipt fails its gate. Return true to swallow the failure, or false to re-throw it.

Push to the cloud

pushReceipt sends one receipt to the outbox. The server re-verifies it before accepting — the same byte-identical check you ran locally — so a tampered or under-signed receipt is rejected at the control plane too.

push.ts
import { pushReceipt } from "@hesohq/sdk"import type { ActionReceipt } from "@hesohq/sdk" const receipt: ActionReceipt = JSON.parse(receiptJson) const result = await pushReceipt(receipt)if (!result.accepted) {  throw new Error(result.rejectionReason ?? "receipt rejected")}console.log(result.receiptId)

It resolves to an OutboxPushResult:

receiptIdstring
The id the control plane assigned the stored receipt.
acceptedboolean
Whether the server accepted the receipt after re-verifying it.
rejectionReasonstring
Present only when accepted is false — why the server turned the receipt away.

To send many at once, pushReceipts takes an array and returns one result per receipt. When an action needs a person in the loop, open an approval with openApproval and block on waitForApproval. It polls until the approver decides or the call times out (defaults: poll every 2000 ms, time out after 300000 ms). The full surface — the cloud client, the gating helpers, and the wire types — lives in the TypeScript SDK reference.

What pushing a receipt proves

Accepting a receipt proves the operator authorized the action under a known policy, and at L1 that a person approved it with a device-held key. It records what was authorized, not whether the action succeeded downstream. The cloud holds no signing key; it only re-checks the signatures already on the receipt.

Next steps