Protocol Internals
This page is the normative reference for HARP v2 (Human Approval Receipt Protocol) as shipped — every byte layout, claim name, and constant here is the one the production platform, apps, and verifier use. Conceptual treatments live in the Architecture and Security Model pages; this is the wire.
The v1 relay protocol (end-to-end encrypted relay envelopes, QR pairing) is superseded and no longer served; its documentation lives in repository history.
Actors and identifiers
Section titled “Actors and identifiers”Four parties — agent backend, platform, approver device, resource server — and prefixed identifiers throughout:
| Prefix | Entity | Prefix | Entity |
|---|---|---|---|
ten_ | tenant | req_ | authorization request |
hu_ | human | rct_ | receipt (jti) |
dev_ | device | pol_ | Cedar policy |
grp_ | group | inv_ | enrollment invite |
key_ | API key (ha_live_…/ha_test_… secret) | ses_ | session |
The two clocks
Section titled “The two clocks”- Request TTL (10 s – 1 h, set at authorize): how long the human has to decide. Expiry mints no receipt — nobody signed anything.
- Receipt expiry (
exp − iat ≤ 600 sfrom mint): how long the agent has to present the proof. Replay stores therefore only ever track ten minutes ofjtis.
Request lifecycle
Section titled “Request lifecycle”POST /v1/authorize (API-key auth; the key is the principal) → platform canonicalizes the plan, stamps plan_hash and the tenant’s required_assurance, creates req_… → fan-out to every registered device → devices respond with co-signatures → policy evaluation (quorum N-of-M with early-deny, unanimous with veto, or a Cedar policy) → receipt minted on decision → outcome delivered by receipt pull (waitForReceipt / GET /v1/requests/:id) and pushed via request.decided / request.expired webhooks to the agent backend. States: pending → delivered → decided, or expired (lazy flip + webhook), or cancelled. Late responses receive 410.
Plan hashing — sha256-canonical-v1
Section titled “Plan hashing — sha256-canonical-v1”plan_hash = hex( SHA-256( JCS(plan) ) )JCS is RFC 8785 JSON Canonicalization. The verifier recomputes this from the request the resource server is actually about to execute and byte-compares — the lying-agent killer. Content limits at issuance: action ≤ 200 chars (^[a-z][a-z0-9_.]*:[a-z][a-z0-9_]*$ — the scope-like service:operation label), description ≤ 4000, plan ≤ 32 768 bytes, reason ≤ 1000.
Receipt v2 — the JWS
Section titled “Receipt v2 — the JWS”Compact JWS, EdDSA, per-tenant key:
header: { "alg": "EdDSA", "kid": "<tenant_id>:k_<n>", "typ": "humanauth-receipt+jws;v=2" }claims: iss = deployment issuer (hosted: https://api.humanauth.ai; self-host: HA_ISSUER) aud = tenant_id sub = human_id jti = rct_<32 hex> iat, nbf, exp (exp − iat ≤ 600) ha = { v: 2, request_id, action, result, plan_hash, plan_alg: "sha256-canonical-v1", approvers: [ApproverEntry…], policy_id?, policy_hash?, rule_satisfied?, org_snapshot_id?, required_assurance? }typ is deliberately not JWT: a strictly-validating auth stack will never accept a receipt as a bearer token. Denials mint full receipts (result: "denied"); expiry mints none.
ApproverEntry (one per co-signer, max quorum 10):
{ huid, device_id, device_pubkey, // base64url raw: Ed25519 32B | P256 65B uncompressed 0x04||X||Y device_alg?, // "Ed25519" (default) | "P256" assurance, // "tap" | "biometric" | "elevated" decided_at, // unix seconds, device clock, server-verified ±300s device_sig, // base64url raw 64B: Ed25519 | ECDSA-P256 r||s (no DER) reason_hash? } // "sha256:<hex>" commitment — plaintext reasons never travelThe device co-signature (normative)
Section titled “The device co-signature (normative)”The message the device signs is the UTF-8 bytes of five fields joined with a two-character || separator:
plan_hash || request_id || huid || device_id || decided_at(e.g. sha256:ab12…||req_9f…||hu_7c…||dev_1a…||1755194742 — decided_at as decimal string). Ed25519 signs the raw bytes; P256 signs ECDSA/SHA-256 with the 64-byte r||s encoding. The platform verifies this before minting against the device’s registered public key, and the verifier re-verifies it offline against the pubkey embedded in the receipt. This is the second, independent signature of the dual-signature chain — a compromised platform key alone cannot fabricate it.
The device-proof header (per respond call)
Section titled “The device-proof header (per respond call)”POST /v1/respond/:request_id additionally requires DPoP-style proof that the caller holds the device key right now:
X-Device-Proof: <base64url(payload_json)>.<base64(signature)>payload: { "htm": "POST", "htu": "/v1/respond/req_…", "iat": <unix s>, "nonce": "<b64 16B>" }Signature over the UTF-8 payload bytes; iat within ±60 s of server time. A stolen session token without the device’s private key cannot approve anything.
Respond body: { human_id, device_id, decision: "approved"|"denied", reason?, device_signature, device_pubkey, assurance?: "tap"|"biometric"|"hardware", decided_at?, form_data? } — wire value hardware surfaces as elevated in receipts. Send decided_at; legacy clients that omit it force the platform to scan a bounded window of candidate seconds.
Operator-mode enrollment and sessions
Section titled “Operator-mode enrollment and sessions”The bare self-host tier (self-hosting guide) replaces IdP sessions with platform-native primitives:
- Invites: admin mints
ha_inv_<26 hex>(stored hash-only) →POST /v1/invites/redeem{ code, device: { pubkey, alg?, name?, push_platform?, push_token? } }atomically claims the code, binds the human, registers the key, returns the first session. The connect QR payload isharp://connect?v=1&url=<platform>&code=<invite>. - Device sessions: two-segment compact tokens
base64url(payload).base64url(sig), platform-Ed25519-signed,typinside the payload —humanauth-challenge+sig(120 s) andhumanauth-session+sig(1 h, Bearer). Mint: fetch a challenge atPOST /v1/sessions/device/challenge, sign the whole challenge token string with the device key, submit toPOST /v1/sessions/device. Deliberately not JWTs, same typ-discipline as receipts.
Discovery
Section titled “Discovery”GET /.well-known/humanauth advertises api_base (deployment-aware), auth_mode (workos | operator), workos_client_id (workos mode), and the platform’s Ed25519 public key — everything an approver app needs to serve any deployment from one build.
Verification
Section titled “Verification”The resource server runs the ten-check offline chain ending in the atomic (jti, idempotencyKey) replay claim — see the Verifier SDK for enforcement and the Build an Approver page for the device-side contract.