# The Handoff — pull your human in at exactly the decision point

**A Handoff is the typed `NEEDS_HUMAN` outcome: how you — the agent — return control to your human at exactly the decision point that needs them, and nowhere else.** The outcome always carries BOTH the full approval `url` (you're on the human's machine: open the default browser) AND a signed, expiring `shortUrl` (you're remote: relay it over chat), plus a `pollUrl` so you resume autonomously the moment approval lands. Not an approval flow. Not a login redirect. A wire type you branch on.

**AX = Agent eXperience — what UX and DX were for humans, AX is for agents.** The protocol home is **AXP — the Agent eXperience Protocol (https://apis.ax/axp)**. The Handoff is the keystone of [B2H2A](https://apis.ax/b2h2a) — and the escape hatch every other motion reaches for the moment a human decision is load-bearing: the consumer approval in [B2A2C](https://apis.ax/b2a2c), the mid-chain authorization in [B2A2B](https://apis.ax/b2a2b), the ceiling raise in [B2A](https://apis.ax/b2a).

---

## The law

A gate has exactly two honest exits. If **money** resolves it, it is a typed `402` carrying an `OFFER`. If a **human decision** resolves it, it is a typed `NEEDS_HUMAN` at `202`. **A gate that is not an `OFFER` or a Handoff is a dead end, and dead ends are conformance failures.**

Everything else — the login wall, the "verify your email to continue," the silent hang — is the dead end the human-first web ships by default: a gate with no machine-legible way through. You hit it, you have nothing to branch on, and the job your principal thought was delegated bounces back to them as homework. The Handoff exists so that never happens: even the gate that genuinely needs a human is a *typed, resumable* outcome, not a wall.

---

## The shape

`NEEDS_HUMAN` ↔ HTTP `202` — accepted, pending human approval; the standard async poll-to-resume shape. Dual delivery is **ALWAYS-BOTH**: a body missing any of the four required members is not a Handoff, and the toolkit refuses to construct one.

```jsonc
// 202 Accepted
{
  "type": "NEEDS_HUMAN",
  "url": "https://api.example.com/approvals/9f2c",   // local: open in the default browser
  "shortUrl": "https://apis.ax/a/9f2c",              // remote: signed, expiring, NOT single-use
  "pollUrl": "https://api.example.com/approvals/9f2c/poll",
  "expiresAt": "2026-07-22T18:40:00Z",
  "code": "WDJB-MJHT"                                 // only when stakes demand it (RFC 8628)
}
```

| member | required | what it is |
|---|---|---|
| `url` | always | The full approval URL. The local route: open it in the human's default browser, in the session they already trust. |
| `shortUrl` | always | Signed, expiring, deliberately **not** single-use. The remote route: relay it over chat/Slack/SMS — wherever the human already is. |
| `pollUrl` | always | The device-flow poll handle. You poll it and resume the moment approval lands. |
| `expiresAt` | always | ISO 8601. The approval window; past it, the pending Handoff is expired, typed as such. |
| `code` | stakes-gated | RFC 8628 user code. Present only when stakes demand it — see below. |

---

## Choose the route by where your human is

Both routes ride every Handoff, so you never negotiate for the delivery you need — you just pick:

- **Human is local** (you run on their machine): open `url` in the default browser. They land on the approval page inside the session they already trust — logins, saved payment methods — approve with one interaction, and control snaps back to you. No code to read, no copy-paste.
- **Human is remote** (you run on a server, a schedule, a CI box, a fleet): relay `shortUrl` over the channel the human is already in. This is **OAuth Device Flow (RFC 8628)** — you hold the poll handle; the human approves on their own device.

---

## The preview cannot approve — the shortUrl survives the unfurl

The `shortUrl` is deliberately **NOT single-use.** Messaging apps unfurl every link they carry, so `GET` on the shortUrl is side-effect-free: the unfurl renders a stakes-aware OpenGraph preview of *exactly what's being approved* — the act, the stakes — and approves nothing. **Only explicit human interaction on the page approves.** A preview can never spend your principal's money.

This is why a single-use token is the wrong design, not a stricter one: the first unfurler to fetch the link would consume the approval before the human ever saw it. The Handoff is built for the channel it travels on. What secures the link is the signature and the expiry — not a use-count that a link preview would burn.

---

## The code is stakes-gated

The verification code exists to bind *this* approval to *this* running agent, so an approval can't be replayed or a request substituted. It appears exactly when that binding is worth a human's extra second:

| situation | `code` | why |
|---|---|---|
| High-stakes act against an **existing account** or **existing payment instrument** — moving real money, acting as an established identity | **present** — surface it; the human confirms it matches | Binds the approval to your request; closes the substituted-request and replay holes when a real credential is at stake. |
| **New signup** or entering **new payment details** — a fresh account, a card typed for the first time | **omitted** | No existing credential to protect, no prior session to bind against. A code here is pure friction; the browser-open or shortUrl is enough. |

---

## Poll to resume

You poll `pollUrl` and resume autonomously the moment approval lands. Every branch is typed:

- **Approved** → the poll answers the resumed outcome (`OK`, or the next gate). You continue inside the Mandate.
- **Declined** → the poll answers a typed `BLOCKED`. You report the branch honestly and stop.
- **Window expires** → you get the pending outcome back annotated `expired: true`. Not a silent hang, not a faked success — a fact you re-plan on.

`ax.resume()` is this whole loop as one call: try the local browser open, else relay the shortUrl through your channel, then poll to resume — and it annotates the result with which delivery happened.

---

## Run it

Everything below ships today.

```bash
npm install agent-experience   # zero-dep: typed NEEDS_HUMAN, parseHandoff, ax.resume, ax.needsHuman
```

**Consume a Handoff** (you're the agent that hit the gate):

```js
import ax from 'agent-experience'

const res = await ax.fetch(capabilityUrl)
const out = await res.outcome()
// typed, always: OK | EMPTY | BLOCKED | OFFER | NEEDS_HUMAN | FOREIGN

if (out.kind === 'NEEDS_HUMAN') {
  const resumed = await ax.resume(out, {
    relay: (shortUrl) => chat.send(shortUrl),  // remote route; local browser-open is tried first
  })
  // resumed.kind    → the typed outcome after approval (or BLOCKED on decline)
  // resumed.delivery → 'opened' | 'relayed'
  // resumed.expired  → true when the window closed instead
}
```

**Emit a Handoff** (you're the surface that needs a human): constructors return real `Response`s — drop one into any Web framework, or into the `ax(handler)` mount kernel.

```js
import ax from 'agent-experience'

return ax.needsHuman({
  url: 'https://api.example.com/approvals/9f2c',
  shortUrl: 'https://apis.ax/a/9f2c',
  pollUrl: 'https://api.example.com/approvals/9f2c/poll',
  expiresAt: '2026-07-22T18:40:00Z',
  code: 'WDJB-MJHT',   // include only when stakes demand it
})
// → a 202 with type NEEDS_HUMAN. Refuses to construct without url, shortUrl,
//   pollUrl, AND expiresAt — dual delivery is ALWAYS-BOTH, by construction.
```

And before you trust a surface to hand off honestly, audit it:

```bash
npx apis.ax audit <domain>   # the pinned conformance gate, judged by api.qa, never self-graded
```

---

## Where the protocol goes from here

State it plainly, because it is the direction, not the present tense. The Handoff *vocabulary* ships — the typed outcome, the ALWAYS-BOTH constructor, the parse, the resume loop, and the unfurl register with its brand OpenGraph pipeline. What **AXP is ratifying now**: `NEEDS_HUMAN` ↔ `202` into the pinned spec's closed outcome table, the api.qa check that fails any Listing whose gate is a dead end rather than an `OFFER` or a Handoff, and the estate-run approval rail — the shortUrl signing service and stakes-aware approval pages — so every gate on the platform emits the shape above. As each clause lands, its claim lands with it. The ratchet only moves one way.

---

## Failure vs. success

**Failure** is the untyped gate: a login wall you cannot branch on, an approval link that a chat preview silently consumed, a poll that hangs forever, a job bounced back to a principal who thought it was delegated.

**Success** is one interruption, correctly placed: you hit the gate, relay one signed link into the channel your human already reads, they see a preview of exactly what's at stake, tap once — confirm a short code only when real money or a real identity is on the line — and you resume mid-task without being restarted. The human decided; you did everything else.

---

*Human twin: https://apis.directory/handoff*
