sandbox:receipt, is released to an endpoint that acts on nothing and answers with a receipt. So you see the whole
path for real: the policy, the human approval, the signed authorization, the release to an external endpoint, and a
signed record you can verify yourself.
The sandbox at a glance
Try it in your browser
Every page in the REST API tab has a Try it panel. It calls the sandbox first, with the demo key already filled in. Start with these, in order:- Get the caller: who the demo key is.
- Get the policy in effect with
capabilitysandbox:receipt: what a request must carry. - Get a demo approval: a signed approval for a target you choose.
- Execute a transaction: your request, with that approval in
signals.approvalArtifact.
Run the whole flow
Each script does seven steps: who am I, what must a request carry, send with no approval (refused), get a demo approval, send with it (approved, released and signed), verify the signed record, send the same approval again (refused). TypeScript and Python use the published SDKs (npm install @parmana/sdk,
pip install "parmana[verify]" requests). The cURL and PowerShell versions need nothing installed. Each script was
run against the live sandbox before it was published here; the files are in
examples/sandbox-playground.
For TypeScript and Python, set the key first: export PARMANA_API_KEY=2VfYWCzt_cBAPK-8uufX6ordfY2JuQhFPsohuEumKME (macOS, Linux) or
$env:PARMANA_API_KEY = "2VfYWCzt_cBAPK-8uufX6ordfY2JuQhFPsohuEumKME" (PowerShell). Then run npx tsx playground.ts or python playground.py.
What each step returns
Every response below is real, captured from the sandbox on 2026-10-02.1
1. Who am I: GET /callers/me
200
sandbox:receipt and nothing else, and only as sandbox-visitor.2
2. What must a request carry: GET /policies/in-effect
Query: Read it like this.
capability=sandbox:receipt.200
policy is the name and version your request must name. signals.facts are the facts the
policy reads, and signals.schema gives their types. signals.bound says note must equal parameters.note,
so you cannot declare one note and release another. signals.approval says receiptApproved is true only with
a signed approval whose resourceId is the request’s target. The server checks that itself, so declaring
true without one is refused.3
3. Send with no approval: refused
403
4
4. Get a demo approval: POST /sandbox/approvals
Body: In production this object is made by a person on their own machine with their own private key
(Human approval). Here the sandbox’s demo approver signs it for anyone. The server
checks it exactly as it checks a real one: a trusted approver key, the signature, the capability, the
{ "capability": "sandbox:receipt", "resourceId": "your target" }201
resourceId against your target, the expiry, and that it was never used before.5
5. Send with the approval: approved, released, signed
Put the whole approval object in
signals.approvalArtifact and set signals.receiptApproved to true. The
response is the signed Execution Trust Record. Abridged here; the full record also carries the request, the
signed authorization and the chain hashes:200 (abridged)
responseSummary is what the receipt endpoint answered after it verified Parmana’s signed release.6
6. Verify the record yourself
Fetch the sandbox’s public key from TypeScript:
GET /keys/default (no key needed) and check the record offline, with no
further call to the server:Offline verification result
verifyExecutionTrustRecordOffline(record, { default: pem }). Python:
verify_execution_trust_record_offline(raw_record, {"default": pem}), with the record as plain JSON (see the
note below). Change one character of the record and valid becomes false. See
Verify independently.7
7. Send the same approval again: refused
403
Python SDK 1.4.0: pass the record to
verify_execution_trust_record_offline
as the plain JSON the server sent, for example from GET /trust-records/ {businessTransactionId}. The decoded model that client.execute() and
client.trust_record() return drops a null previousChainHash, so
verifying the model reports a hash mismatch although the record is intact. The
TypeScript SDK is not affected, and the next Python release fixes it.Every refusal you can meet
All real, from the sandbox.
Every code is explained in the Error catalog.
Rules for a raw HTTP request
The SDK builders (createBusinessTransaction, create_business_transaction) follow these for you. Without an SDK:
businessTransactionIdis a new UUID for every request, andmetadata.businessTransactionIdis the same UUID.intent.actionissandbox:receipt.intent.targetis any text you choose, and the approval’sresourceIdmust equal it.intent.parameters.noteandsignals.noteare the same text.policyis{ "name": "sandbox-receipt", "version": "1.0.0", "schemaVersion": "1.0.0" }, asGET /policies/in-effectsays.authority.principalIdissandbox-visitor.authorityId,authorizationIdandintentIdare any text, linked as in the scripts.signals.approvalArtifactis the approval object exactly asPOST /sandbox/approvalsreturned it, within 5 minutes.
From the sandbox to your own system
For AI agents
To test an integration before you have your own deployment: base URLhttps://parmana-sandbox.vercel.app, bearer key
2VfYWCzt_cBAPK-8uufX6ordfY2JuQhFPsohuEumKME, capability sandbox:receipt. Read GET /policies/in-effect?capability=sandbox:receipt first and
build the request from its answer. Get the approval from POST /sandbox/approvals with resourceId equal to your
target. Send POST /execute with a client timeout of 120 seconds. Treat 403 POLICY_DENIED as a final refusal, not
something to retry. The full guide is Integrate Parmana, and the machine readable spec is
openapi.yaml.