> ## Documentation Index
> Fetch the complete documentation index at: https://paperplane-justin-winter-s-projects.vercel.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Send Policies

> The named spend-safety layer behind every send: confirmation tokens, idempotency keys, capability tokens, and the sandbox/live split.

Every mechanism on this page already exists and is enforced by the server on
every `POST /v1/orders` call — this page doesn't add behavior, it gives the
existing spend-safety layer one name and one home: **Send Policies**.

Send Policies is the set of rules paperplane enforces so that an autonomous
agent can be trusted with something a support script never could — spending
real money on an irreversible, physical action. Four mechanisms make up the
policy, and all four apply together on every send:

| Mechanism                                   | Answers                                   | Bound to                              | Lifetime               |
| ------------------------------------------- | ----------------------------------------- | ------------------------------------- | ---------------------- |
| [Confirmation tokens](#confirmation-tokens) | Did you actually price this exact letter? | Recipient, content, mail class, price | 30 minutes, single-use |
| [Idempotency keys](#idempotency-keys)       | Is this a retry, not a second letter?     | Your chosen key string                | Life of the order      |
| [Capability tokens](#capability-tokens)     | Are you allowed to act on this order?     | Order id + specific action            | 7 days                 |
| [Sandbox / live split](#sandbox-vs-live)    | Is real money and real mail involved?     | The `sandbox` flag on the order       | Per-request            |

Because paperplane has no accounts and no API keys, none of these can fall
back on "the caller is authenticated." Every guarantee has to come from the
token itself — that's why Send Policies is built from signed, narrowly-scoped
tokens rather than a session or a role.

## Confirmation tokens

A send is never the first call. `POST /v1/quotes` (or the `quote_letter` MCP
tool) is free, creates nothing, and returns a `confirmation_token` (`ppq_`)
cryptographically bound to the exact recipient, content, mail class, and
price it just quoted.

`POST /v1/orders` on the agent path requires that token, and the server
re-derives the binding from the order body — if anything changed since the
quote, the token doesn't match and the order is refused. This guarantees an
agent can never send a letter for content it never priced, and a human can
never be charged for something other than what they approved.

Confirmation tokens are:

* **Single-use.** Redeeming one sends the letter; presenting it again returns
  `confirmation_used`, not a second letter.
* **Short-lived.** 30-minute TTL — `confirmation_expired` past that window,
  and quoting again is the only recovery.
* **Exact-match.** Any change to recipient, content, mail class, or price
  invalidates it (`confirmation_invalid`).

See the full set of confirmation-related codes in the
[error contract](/docs/guides/errors).

## Idempotency keys

Confirmation tokens stop an agent from sending content it never priced;
idempotency keys stop a retry from sending it twice. Pass an
`Idempotency-Key` header on every `POST /v1/orders` call — a retried request
with the same key returns the original order (`"replayed": true`, same id)
instead of creating a new one.

This is what makes retrying after a timeout safe by default: a `429`, `503`,
or dropped connection is never a reason to worry about a duplicate charge or
a duplicate letter, as long as the retry reuses the same key. See
[Retry semantics](/docs/guides/errors#retry-semantics) for how this combines with
each error code.

**Idempotency keys are scoped per caller, never global.** The same key string
used by two different callers (your API key id, else your signed-in account,
else the client address the edge observed) claims two independent orders —
one caller can never be handed back another caller's order, or that order's
`cancel_token`/`review_token`, by reusing a common key like `"1"` or today's
date. If two requests are genuinely retries of the same send, reuse the same
key **from the same caller**; a new key, or a key sent by a different caller,
always mails (or attempts) a new letter.

An in-flight duplicate — the first request with a key hasn't finished yet —
returns `idempotency_key_in_flight` (409, `Retry-After: 5`) rather than
racing a second create; retry with **the same key** to receive that first
request's order once it lands.

## Capability tokens

Because there's no account to authenticate, doing something to an existing
order — after it's been created — needs its own signed grant. Capability
tokens (`ppc_`) are minted at order creation and returned under
`capability`:

| Token          | Grants                                           | Required by                                       |
| -------------- | ------------------------------------------------ | ------------------------------------------------- |
| `cancel_token` | Cancel the order + release any payment hold      | `DELETE /v1/orders/{id}` via `X-Capability-Token` |
| `review_token` | Post a review attributed from the return address | `POST /v1/reviews`                                |

Both are bound to the specific order id **and** the specific action — a
`cancel_token` can't be replayed against `/v1/reviews`, and vice versa — and
both expire after **7 days**. Possession of an order id alone is never
enough to cancel or review it; see [Cancel & track](/docs/guides/cancel-track) for
the full cancellation flow, including why the state has to be cancelable in
the first place.

Capability errors are 403s: `invalid_capability` when the token doesn't
match this order + action, `expired_capability` past the 7-day window.

## Sandbox vs live

The fourth piece of the policy is which world a send happens in.
[Sandbox mode](/docs/sandbox) runs the complete pipeline — rendering, address
verification, screening, simulated fulfillment with a mock tracking number —
instantly and free, with `sandbox: true` and zero keys. Live mode is the
same call without that flag, and requires either a human-approved Stripe
`payment_url` or a prepaid `credit_code` before anything prints.

Confirmation tokens, idempotency keys, and capability tokens all apply
identically in both modes — sandbox is not a relaxed policy, it's the same
policy with payment and physical mailing swapped for simulation. That's what
lets an agent (or a person evaluating the API) exercise the full send policy
before any card or real mail is involved.

## Send Policies at a glance

```
quote_letter          → confirmation_token (ppq_, 30 min, single-use)
send_letter           → requires confirmation_token + Idempotency-Key
                         requires payment_url / credit_code, unless sandbox: true
                       ← capability { cancel_token, review_token } (ppc_, 7 days)
cancel / review       → requires the matching capability_token
```

Every layer fails closed: no token means no action, an expired or mismatched
token means no action, and a replayed idempotency key means no second order.
An agent following this contract can be handed a payment method and mail
real letters without a human in the loop for anything except the payment
approval itself — see [Build with agents](/docs/agents) for why that's a
reasonable thing to automate.
