Skip to main content
The normative text lives in the repository at spec/handoff-protocol-v0.1.md, written with RFC 2119 keywords, alongside the OpenAPI 3.1 wire contract (spec/openapi.yaml), JSON Schemas, and the signing scheme with reproducible test vectors (spec/signing.md). This page is an orientation, not a replacement; where they disagree, the spec wins.

The object model

Five objects carry the protocol. Each has its own state machine in the spec.

Declarations, not kinds

There is no kind enum on the wire and no branch behind one. A request declares three things: the shape of the answer (answer.fields), what the person must be handed to decide (capabilities, as opaque handles their own client resolves), and the authority the answer requires (role, freshness, quorum). All eight interaction patterns the spec requires are eight declarations over that one shape. Two consequences worth knowing before you design against it:
  • A secret field never carries its value through the protocol. The answer says {"provided": true} and the value goes to a sink your runtime owns and can audit (§12, out-of-band secret custody). Declaring a secret field also raises the authority floor server-side, as a consequence of the request’s shape.
  • Unknown field types are rejected at raise time rather than degraded. A field a renderer cannot draw is a field a human cannot answer.

Identity, and who may answer

Requests have three deliberately distinct keys: the server-minted id, your dedupe_key (what makes two raises “the same request”), and your waiter_ref (what a successor process presents to resume the wait). Identity is tenant-scoped throughout. The requester may not answer its own request (§4.2), an identifier is not an authorization (§4.6), and a delivery channel never confers authority (§4.7). Treat a request id as a capability: in this protocol it can be one, so it belongs in the same custody as a credential.

Defaults are declared, not guessed

A request that can proceed without an answer declares that at raise time, as a TTL policy (on_expiry: "default" with the default value). When it fires, the server mints a policy receipt with actor.type = "policy", so no audit can mistake a timeout for consent. Guessing the same value client-side afterwards produces identical behaviour and no record at all; the difference between those two is most of the reason to use the protocol.

Receipts are evidence, not logs

A receipt binds the actor, the authority they held, and the exact presentation the decision was made against (§9.3), and it lands on a hash chain (signing.md §2.2) that any reader can walk with the standard library. The chain makes tampering evident; it does not make the operator honest, which is why the project says plainly that independent attestation requires a party who is not the operator. Corrections are new receipts that reference the old one. Nothing is edited in place.

Callbacks

Outbound callbacks are signed over the raw bytes on the wire. Verification rules that both SDKs enforce:
  • Verify the bytes as received; re-encoding a parsed body changes the hash.
  • A valid signature proves the sender, never the tenant. Resolve tenancy from your own stored state, keyed on the endpoint or the secret, and never from a field in the body.
  • delivery_id is your deduplication key.

Errors

Every error carries a stable machine-readable code (§13). Branch on the code, never the message. The SDKs raise typed classes that mirror the codes and pass unknown codes through intact rather than coercing them into the nearest familiar class.

Level 2 exists and is deliberately not implemented

Conformance Level 2 adds the continuation extension (§14): a server that stores an encrypted resume_payload alongside the wait. The reference implementation refuses such payloads, because it has no encryption at rest to protect one with, and it advertises Level 1, derived from that same capability check so the advertisement and the behaviour cannot drift.