Skip to content

HTTP API Reference

Marcus Richards edited this page Oct 7, 2026 · 2 revisions

HTTP API Reference

Taken from the route code in server.py and dashboard/server.py at 9900817. Both use the stdlib http.server (ThreadingHTTPServer). JSON in, JSON out. No caller authentication.

Permit service: server.py

python server.py [--port 8741] [--sandbox]. Binds 127.0.0.1. Default port 8741. Mock rail unless --sandbox is given.

Path ids match [\w-]+. Errors look like {"error": "<reason>"}. A missing or unparseable JSON body is treated as {}. Unmatched routes return 404 not_found.

Method Path Purpose Body Success Errors
POST /api/permits Issue a permit (GRANTED) agent_id, cap_cents, allowlist, expiry_hours (default 1) 201 {permit_id, receipt} 400 missing fields
GET /api/permits/<permit_id> Permit state — 200: cap, allowlist, expiry, revoked, reserved/captured/remaining cents, in_flight count, parent_id 404 unknown_permit
POST /api/permits/<permit_id>/check Read-only gate check. No reservation, no receipt amount_cents, merchant_id 200 {allowed, reason} 400 non-positive or non-int amount
POST /api/permits/<permit_id>/spend Spend attempt through the pipeline amount_cents, merchant_id, predicate (default delivery_hash), artifact_hash 200 {allowed, reason, escrow_id, receipts}. Sandbox: 202 {status: approval_required, operation_id, order_id, approval_url, reservation: held, next} 400 bad fields or amount
POST /api/operations/<operation_id>/resume Resume a sandbox spend after payer approval (same order) {} 200 like spend, or 202 if still unapproved 404 unknown_operation
POST /api/permits/<permit_id>/delegate Carve a sub-permit agent_id, cap_cents, allowlist, expiry_hours (default 1) 201 {permit_id, parent_id, receipt} 404 unknown_parent; 400 block reason
POST /api/permits/<permit_id>/tighten Narrow a live permit (cascades) any of cap_cents, approval_threshold_cents, remove_merchants, expiry (ISO-8601), actor 200 {tightened, permit_id, changes, receipt} 400 widen, no-op or invalid; 404
POST /api/permits/<permit_id>/estop Revoke this permit and void its in-flight holds {} 200 {revoked, voided_escrows, receipt} 404 unknown_permit
POST /api/permits/<permit_id>/revoke-cascade Revoke the permit and its subtree, void holds, release carves {} 200 {revoked, cascaded_to, voided_escrows, receipt} 404 unknown_permit
POST /api/escrows/<escrow_id>/release Submit evidence → predicate → capture delivered_bytes_b64 or acceptance_signature 200 {released, reason, capture_id} 400 invalid base64
POST /api/escrows/<escrow_id>/reconcile Resolve an UNKNOWN escrow {} 200 {resolved, outcome, capture_id, receipt} —
POST /api/escrows/<escrow_id>/retry-cleanup Retry a stuck void {} 200 {cleanup_cleared} —
GET /api/ledger Full receipt chain + verification — 200 {chain: {ok, reason}, receipts: [...]} —
GET /debug/paypal Mock call counts (authorizations, captures, voids) — 200 404 in sandbox mode

That is 14 routes: 3 GET and 11 POST.

Behaviour worth knowing

  • Refusals return 200. A BLOCKED spend, a REFUSED release or a pending approval comes back as HTTP 200 with allowed/released: false and a reason.
  • Pending approval: the spend response has no top-level approval_id. It is in the APPROVAL_PENDING receipt payload inside receipts.
  • cap_cents coercion: /api/permits and /delegate run int(body["cap_cents"]). In a local probe at 9900817, 50.7 was granted as 50, "5000" as 5000, and true as 1, and a delegate with 10.9 carved 10. 0.5 (which becomes 0) raised inside the handler and the connection closed with no HTTP response. /check, /spend and /tighten reject non-int values with 400.
  • No approval endpoints on this service, and the grant takes no threshold. See Principal Approvals.
  • /estop does not cascade, despite the module docstring. See E-Stop and Revocation.
  • Pending sandbox operations are held in memory (PENDING_OPS).

Dashboard: dashboard/server.py

Started in-process by the LLM demos (default port 8471; demo_approval.py uses 8474). It can also run standalone with empty state: python3 dashboard/server.py.

Method Path Purpose
GET / dashboard/index.html
GET /api/state Permits (including effective_cap_cents), escrows, receipts, chain_ok, pending approvals (the page polls every second)
POST /api/estop {permit_id} → flow.estop() (single permit, not a cascade)
POST /api/revoke-cascade {permit_id} → flow.revoke_cascade() (whole subtree)
POST /api/tighten {permit_id, cap_cents}: narrow the cap only. A non-int or non-positive cap_cents gives {ok: false}
POST /api/approvals/<approval_id>/approve Principal approves (actor dashboard)
POST /api/approvals/<approval_id>/deny Principal denies

Example (mock)

python server.py &
curl -s -X POST localhost:8741/api/permits -d '{"agent_id":"a1","cap_cents":5000,"allowlist":["m1"],"expiry_hours":1}'
curl -s -X POST localhost:8741/api/permits/<permit_id>/check -d '{"amount_cents":3000,"merchant_id":"m1"}'
curl -s localhost:8741/api/ledger

Clone this wiki locally