{"openapi":"3.1.0","info":{"title":"paperplane API","version":"1.0.0","summary":"Send real physical mail from code.","description":"Upload a PDF or paste text; we print it and hand it to USPS the next business day. No account required — pay per letter with a Stripe link or a prepaid credit code, and use `sandbox: true` to exercise the whole flow for free.\n\nEvery failure returns the same envelope: `status`, a stable machine-readable `code`, a human `reason`, and a `next` array of concrete recovery steps.\n\nThis document is generated from the same zod schemas the server validates with, so it cannot describe an endpoint that does not exist.","contact":{"name":"paperplane support","url":"https://www.sendpaperplane.com/contact"},"license":{"name":"Proprietary","identifier":"LicenseRef-Proprietary"},"termsOfService":"https://www.sendpaperplane.com/terms"},"servers":[{"url":"https://www.sendpaperplane.com","description":"Production"}],"tags":[{"name":"Mail","description":"Quote, send, track, and cancel letters."},{"name":"Credits","description":"Prepaid balances that pay for letters without a card."},{"name":"Reviews","description":"Post-delivery feedback. Identity is derived from the order, not supplied."},{"name":"Addresses","description":"Typeahead and reverse geocoding. Convenience only; USPS verification is authoritative."},{"name":"Meta","description":"Machine-readable description of this API."}],"paths":{"/v1/quotes":{"post":{"operationId":"createQuote","summary":"Price a letter before sending it","description":"Free — creates nothing and never touches payment. The price is all-in (postage, printing and the envelope included); `breakdown` lists the piece and any options that make up the total. Quoting first is the recommended shape for agents: it is the safe call, and it tells the user what a send will cost.\n\nSupply `to` plus `text` (or `pdf_url`) and the response also carries a single-use `confirmation_token` bound to that exact letter and price. `POST /v1/orders` requires one on the agent path, so this is where a send begins.","tags":["Mail"],"security":[{},{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteRequest"},"example":{"mail_class":"first_class","page_count":1,"color":false,"tracking":false}}}},"responses":{"200":{"description":"Priced quote. `confirmation_token`, `expires_in_minutes`, and `next` appear only when `to` and content were supplied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponse"},"example":{"status":"ok","total_cents":199,"total":"$1.99","breakdown":[{"id":"first_class_letter_1_page","label":"First-Class letter, 1 page","amount_cents":199}],"note":"All-in price. Postage, printing and the envelope are included; the breakdown lists what makes up the total."}}}},"400":{"description":"Invalid request, or the letter violates a pricing rule (page limit, tracking on a class that already includes it).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_key` — an Authorization header was sent and it did not verify. A presented credential is never silently downgraded to anonymous.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`key_disabled`, or `insufficient_scope` when the key is valid but does not carry the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`key_store_unavailable` — the key could not be checked, so it is not honoured. Retry, or omit the header to call anonymously.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/orders":{"post":{"operationId":"createOrder","summary":"Send a letter","description":"Renders the document, prices it, verifies both addresses with USPS, then either charges a prepaid credit code or returns a Stripe payment link. **This spends money and mails a physical object to a real address.** Charges the sender once payment is captured — immediately if a credit code covers it, otherwise when the returned `payment_url` is paid — or spends a sandbox no-op if `sandbox: true` for a free, instant, end-to-end simulation. Provide exactly one of `text`, `upload_key`, `pdf_base64`, or `pdf_url`.\n\nSend an `Idempotency-Key` header on every call. A repeat with the same key returns the original order with `replayed: true` and HTTP 200 instead of mailing a second letter, which makes a timeout safe to retry.\n\nOn the agent path a `confirmation_token` from `POST /v1/quotes` is required; without one the call fails `428 confirmation_required` rather than sending.","tags":["Mail"],"security":[{},{"bearerAuth":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Caller-chosen unique string. A retry with the same value replays the original order (HTTP 200, `replayed: true`) instead of creating a second one.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrder"},"example":{"mail_class":"first_class","to":{"name":"Jordan Rivera","line1":"742 Evergreen Terrace","city":"Springfield","state":"IL","zip":"62704"},"from":{"name":"Alex Chen","line1":"221B Baker St","city":"Springfield","state":"IL","zip":"62701"},"text":"Dear Jordan,\n\nThis is a letter sent through paperplane.\n\nBest,\nAlex","sandbox":true}}}},"responses":{"200":{"description":"Idempotent replay. The `Idempotency-Key` was already used, so the original order is returned with `replayed: true` and nothing new was created or charged. Distinguishable from a fresh send by the status code alone.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrderResponse"},"example":{"status":"ok","order":{"id":"ord_test_1a2b3c4d5e6f7a8b","status":"submitted","sandbox":true,"mail_class":"first_class","page_count":1,"color":false,"to":{"name":"Jordan Rivera","city":"Springfield","state":"IL","zip":"62704"},"price_cents":199,"breakdown":[{"id":"first_class_letter_1_page","label":"First-Class letter, 1 page","amount_cents":199}],"tracking_number":null,"expected_delivery_date":null,"refusal_reason":null,"created_at":"2026-08-23T12:00:00.000Z","updated_at":"2026-08-23T12:00:00.000Z"},"replayed":true,"capability":{"cancel_token":"ppc_a1b2c3d4","review_token":"ppc_e5f6g7h8"}}}}},"201":{"description":"Order created. `status: \"ok\"` means it is paid and moving; `status: \"action_required\"` means a payment step is outstanding — `checkout_ui_mode` says whether that's a `payment_url` to open (`\"hosted\"`, the default) or a `payment_client_secret` to mount in place (`\"embedded\"`). A replay of an order still `pending_payment` can also return `action_required` with neither field — its payment step was minted for the original request; poll `GET /v1/orders/{id}` to resume it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrderResponse"},"example":{"status":"ok","order":{"id":"ord_test_1a2b3c4d5e6f7a8b","status":"submitted","sandbox":true,"mail_class":"first_class","page_count":1,"color":false,"to":{"name":"Jordan Rivera","city":"Springfield","state":"IL","zip":"62704"},"price_cents":199,"breakdown":[{"id":"first_class_letter_1_page","label":"First-Class letter, 1 page","amount_cents":199}],"tracking_number":null,"expected_delivery_date":null,"refusal_reason":null,"created_at":"2026-08-23T12:00:00.000Z","updated_at":"2026-08-23T12:00:00.000Z"},"capability":{"cancel_token":"ppc_a1b2c3d4","review_token":"ppc_e5f6g7h8"}}}}},"400":{"description":"`validation_error` or a document error (`invalid_pdf`, `empty_document`, `pdf_fetch_failed`, `page_limit_exceeded`, …). Same input will fail the same way.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_key` — an Authorization header was sent and it did not verify. A presented credential is never silently downgraded to anonymous.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"The credit code is unknown, or has insufficient balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`key_disabled`, or `insufficient_scope` when the key is valid but does not carry the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`confirmation_mismatch` — the letter does not match the quote the token was issued for — or `confirmation_used`, meaning that token already mailed a letter: re-quote, do not resend. Or `idempotency_in_flight`, meaning an earlier request with this `Idempotency-Key` has not finished: retry with the SAME key after `Retry-After` to receive its order, never with a new one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"USPS cannot deliver to the recipient address as entered, or the fulfillment provider terminally rejected the letter.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"428":{"description":"`confirmation_required` — no `confirmation_token` was supplied on a surface that requires one. Quote first, then send with the token. `status` is `action_required`, not `failed`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Velocity cap reached (5/day, 20/month per sender fingerprint), or a per-credential spend cap.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","example":60}}}},"503":{"description":"Transient fulfillment failure (safe to retry), no rail configured for the requested format, or the API-key store was unreachable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/orders/{id}":{"get":{"operationId":"getOrder","summary":"Fetch an order","description":"Free — read-only, no charge. Poll this for status after a send. Terminal statuses are `delivered`, `refused`, `canceled`, and `failed`; the rest (`draft`, `pending_payment`, `screening`, `held_for_review`, `submitted`, `mailed`) still move. Prefer `webhook_url` on the order over polling.","tags":["Mail"],"security":[{},{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Order id, e.g. ord_1a2b3c4d5e6f7a8b (sandbox orders are ord_test_...).","schema":{"type":"string"}}],"responses":{"200":{"description":"The order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderResponse"},"example":{"status":"ok","order":{"id":"ord_1a2b3c4d5e6f7a8b","status":"mailed","sandbox":false,"mail_class":"first_class","page_count":1,"color":false,"to":{"name":"Jordan Rivera","city":"Springfield","state":"IL","zip":"62704"},"price_cents":199,"breakdown":[{"id":"first_class_letter_1_page","label":"First-Class letter, 1 page","amount_cents":199}],"tracking_number":null,"expected_delivery_date":null,"refusal_reason":null,"created_at":"2026-08-23T12:00:00.000Z","updated_at":"2026-08-23T14:30:00.000Z"}}}}},"401":{"description":"`invalid_key` — an Authorization header was sent and it did not verify. A presented credential is never silently downgraded to anonymous.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`key_disabled`, or `insufficient_scope` when the key is valid but does not carry the scope this route needs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No order with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`key_store_unavailable` — the key could not be checked, so it is not honoured. Retry, or omit the header to call anonymously.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"cancelOrder","summary":"Cancel an order","description":"Free — releases any payment hold rather than charging anything, so it requires the `cancel_token` the order creation response returned. Send it in the `X-Capability-Token` header (`X-Cancel-Token` is accepted as a legacy alias). Only possible before the letter is handed to the provider; once submitted, cancellation returns 409.","tags":["Mail"],"security":[{},{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"description":"Order id, e.g. ord_1a2b3c4d5e6f7a8b (sandbox orders are ord_test_...).","schema":{"type":"string"}},{"name":"X-Capability-Token","in":"header","required":true,"description":"Signed cancel token returned by POST /v1/orders for this order, as `capability.cancel_token`. Valid for 7 days. Both capability tokens carry the `ppc_` prefix — they are told apart by the action they were signed for, not by their prefix.","schema":{"type":"string","pattern":"^ppc_"}}],"responses":{"200":{"description":"The canceled order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderResponse"},"example":{"status":"ok","order":{"id":"ord_1a2b3c4d5e6f7a8b","status":"canceled","sandbox":false,"mail_class":"first_class","page_count":1,"color":false,"to":{"name":"Jordan Rivera","city":"Springfield","state":"IL","zip":"62704"},"price_cents":199,"breakdown":[{"id":"first_class_letter_1_page","label":"First-Class letter, 1 page","amount_cents":199}],"tracking_number":null,"expected_delivery_date":null,"refusal_reason":null,"created_at":"2026-08-23T12:00:00.000Z","updated_at":"2026-08-23T12:05:00.000Z"}}}}},"401":{"description":"`invalid_key` — an Authorization header was sent and it did not verify. A presented credential is never silently downgraded to anonymous.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`invalid_capability` — missing, malformed, or issued for a different order or action. Also `key_disabled` / `insufficient_scope` when an API key is presented.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No order with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`too_late` — the letter has already been submitted for printing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`expired_capability` — the cancel token is past its 7-day window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`key_store_unavailable` — the key could not be checked, so it is not honoured. Retry, or omit the header to call anonymously.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/credits/{code}":{"get":{"operationId":"getCreditBalance","summary":"Look up a prepaid credit balance","description":"Balances are split in two: `face_cents` is paid-for value (5 years, per the CARD Act) and `bonus_cents` is promotional value (12 months, spent first).","tags":["Credits"],"parameters":[{"name":"code","in":"path","required":true,"description":"Credit code, formatted pp-xxxx-xxxx-xxxx. Case-insensitive — the route lower-cases before lookup.","schema":{"type":"string","pattern":"^[Pp][Pp]-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}$"}}],"responses":{"200":{"description":"Spendable balance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditBalanceResponse"},"example":{"status":"ok","code":"pp-ab12-cd34-ef56","balance_cents":5400,"face_cents":5000,"bonus_cents":400,"bonus_expires_at":"2027-08-23T00:00:00.000Z","face_expires_at":"2031-08-23T00:00:00.000Z"}}}},"404":{"description":"No credit account with that code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/credits/checkout":{"post":{"operationId":"createCreditCheckout","summary":"Buy a credit pack","description":"Returns a Stripe-hosted checkout URL (default) or, with `checkout_ui_mode: \"embedded\"`, a Stripe Embedded Checkout `payment_client_secret` to mount in place. The credit code is emailed once payment settles either way.","tags":["Credits"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditCheckoutRequest"},"example":{"pack":"value","email":"jordan@example.com"}}}},"responses":{"200":{"description":"Checkout link (`checkout_ui_mode: \"hosted\"`, the default) or a Stripe Embedded Checkout `payment_client_secret` (`checkout_ui_mode: \"embedded\"`) — a discriminated union keyed on `checkout_ui_mode`, never both.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditCheckoutResponse"},"example":{"status":"ok","checkout_ui_mode":"hosted","payment_url":"https://checkout.stripe.com/c/pay/cs_test_a1b2c3"}}}},"400":{"description":"`unknown_pack` or a malformed email.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"`no_checkout_url` — Stripe created the session but returned no URL to send the payer to.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"`payments_not_configured` — Stripe is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/x402/credits":{"post":{"operationId":"buyCreditsWithX402","summary":"Buy a credit pack via x402 (agent payments)","description":"Agent-payable alternative to /v1/credits/checkout for funded agents that can't open a human payment_url. Follows the x402 v2 handshake: call once without an X-PAYMENT header to get a 402 challenge, then retry with the header set to a signed payment payload. Disabled on deployments without a configured facilitator — see docs/PRD-agentic-payments.md. Pass `sandbox: true` to mint a real credit code with no facilitator call and no charge, on any deployment.","tags":["Credits"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/X402CreditsRequest"},"example":{"pack":"value","email":"jordan@example.com"}}}},"responses":{"200":{"description":"Settlement replay — this transaction hash already minted an account, so the same credit code is returned with `replayed: true` and no `settlement`. Nothing was charged twice.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/X402CreditsResponse"},"example":{"status":"ok","credit_code":"pp-ab12-cd34-ef56","face_cents":1999,"bonus_cents":700,"replayed":true}}}},"201":{"description":"Credits minted (or a free sandbox: true rehearsal)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/X402CreditsResponse"},"example":{"status":"ok","credit_code":"pp-ab12-cd34-ef56","face_cents":1999,"bonus_cents":700,"settlement":{"network":"eip155:8453","tx_hash":"0xdeadbeef"}}}}},"400":{"description":"Unknown pack id or malformed email.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"No X-PAYMENT header (challenge returned instead) or the payment payload failed facilitator verification.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`region_unsupported` — the stablecoin lane is hidden in this region (New York).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"description":"x402 is not enabled on this deployment yet — use /v1/credits/checkout or a credit_code instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/uploads":{"post":{"operationId":"createUploadTicket","summary":"Get a signed URL to upload a PDF","description":"Mint a one-shot signed URL so the PDF goes straight to storage instead of base64 through this API. Returns an opaque `upload_key` to pass as `upload_key` on POST /v1/orders. No auth, so a crude per-IP throttle bounds abuse; the bucket enforces 10MB and `application/pdf`, the order path enforces the velocity caps, and the 30-day sweep collects anything abandoned.","tags":["Mail"],"responses":{"201":{"description":"Upload ticket","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadTicketResponse"},"example":{"status":"ok","upload_key":"upl_a1b2c3d4e5f6","upload_url":"https://storage.paperplane.app/uploads/upl_a1b2c3d4e5f6?signature=...","method":"PUT","headers":{"Content-Type":"application/pdf"},"expires_in":900,"max_bytes":10485760,"next":["PUT your PDF bytes to upload_url","Pass upload_key as upload_key on POST /v1/orders"]}}}},"429":{"description":"Rate limited (10/minute per IP).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","example":60}}}}}}},"/v1/uploads/{key}":{"put":{"operationId":"putUploadedFile","summary":"Upload PDF bytes (keyless dev only)","description":"Keyless-dev upload sink. With storage configured, POST /v1/uploads hands back a real signed URL and the browser never touches this route — which is why it hard-refuses unless storage is the in-memory fallback. It exists so the two-step upload flow is the only client path, dev included.","tags":["Mail"],"parameters":[{"name":"key","in":"path","required":true,"description":"Opaque key returned by POST /v1/uploads. Anything else is refused with 400 before storage is ever touched.","schema":{"type":"string","pattern":"^up_[0-9a-f]{32}\\.pdf$"}}],"responses":{"204":{"description":"Bytes stored; pass the key as `upload_key` on POST /v1/orders."},"400":{"description":"upload_key is not a key this server issued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"This route only exists in keyless dev mode.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"PDF exceeds the 10MB limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/csv-map":{"post":{"operationId":"mapCsvHeaders","summary":"Propose a header→field mapping for a bulk import","description":"Parsing happens in the browser, so only the header row and up to three sample rows ever leave the device — never the full recipient list. Advisory: the user confirms or overrides every field before a row becomes an order.","tags":["Mail"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CsvMapRequest"},"example":{"headers":["Full Name","Street","Unit","City","State","Zip Code"],"samples":[["Jordan Rivera","742 Evergreen Terrace","Apt 4","Springfield","IL","62704"]]}}}},"responses":{"200":{"description":"Proposed mapping plus its source","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CsvMapResponse"},"example":{"status":"ok","mapping":{"name":["Full Name"],"line1":["Street"],"line2":["Unit"],"city":["City"],"state":["State"],"zip":["Zip Code"]},"source":"heuristic"}}}},"400":{"description":"Malformed body — send `{ headers: string[], samples?: string[][] }`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (10/minute per IP).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","example":60}}}}}}},"/v1/reviews":{"get":{"operationId":"listReviews","summary":"List published reviews","description":"Only 4- and 5-star reviews written about a real delivered order are published, newest first, capped at 24. `sample: true` marks an illustrative entry rather than a real customer; the live wall no longer seeds any, so in practice the flag is absent or false.","tags":["Reviews"],"responses":{"200":{"description":"Published reviews, newest first","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewListResponse"},"example":{"status":"ok","reviews":[{"id":"rev_a1b2c3d4","stars":5,"comment":"Sent a certified letter without leaving my desk. Arrived in 3 days.","name":"Jordan R.","city":"Springfield","state":"IL","category":"certified","created_at":"2026-08-10T09:00:00.000Z"}]}}}}}},"post":{"operationId":"createReview","summary":"Review a delivered order","description":"One review per order. The reviewer identity (name, city, state) is derived server-side from the order return address — it is never accepted from the caller. Requires the `capability_token` returned by the order creation response: publishing attribution is a public action, so the order id alone is not enough.","tags":["Reviews"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateReview"},"example":{"order_id":"ord_1a2b3c4d5e6f7a8b","stars":5,"comment":"Sent a certified letter without leaving my desk. Arrived in 3 days.","capability_token":"ppc_e5f6g7h8"}}}},"responses":{"201":{"description":"The recorded review","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviewResponse"},"example":{"status":"ok","review":{"id":"rev_a1b2c3d4","stars":5,"comment":"Sent a certified letter without leaving my desk. Arrived in 3 days.","name":"Jordan R.","city":"Springfield","state":"IL","category":"certified","created_at":"2026-08-23T12:00:00.000Z"}}}}},"400":{"description":"Malformed body, or `invalid_stars` for a value outside 1-5.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`invalid_capability` — no `capability_token`, or one issued for a different order or action. The review token is the `ppc_`-prefixed `capability.review_token` from the order response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No order with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`not_reviewable` — that order is not delivered, or has already been reviewed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"`expired_capability` — the review token is past its 7-day window.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/suggest-class":{"post":{"operationId":"suggestMailClass","summary":"Suggest a mail class for a draft","description":"Advisory only, never authoritative — the sender always picks. LLM-backed when ANTHROPIC_API_KEY is set, word-bounded heuristic otherwise.","tags":["Mail"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuggestClassRequest"},"example":{"text":"This letter serves as formal notice that your lease will not be renewed."}}}},"responses":{"200":{"description":"A suggestion plus one sentence of reasoning","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuggestClassResponse"},"example":{"status":"ok","suggested_class":"certified","reason":"This reads as a formal legal notice, where proof of delivery matters.","source":"heuristic"}}}},"400":{"description":"Missing or oversized text.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (10/minute per IP).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","example":60}}}}}}},"/v1/address/autocomplete":{"get":{"operationId":"autocompleteAddress","summary":"Address typeahead","description":"Server-side proxy so the geocoding key never reaches a browser. Returns `enabled: false` with no suggestions when no key is configured. Suggestions are convenience only — USPS verification at order time remains authoritative.","tags":["Addresses"],"parameters":[{"name":"q","in":"query","required":false,"description":"Partial address. Missing, or fewer than 3 characters, returns no suggestions rather than an error.","schema":{"type":"string","maxLength":120}}],"responses":{"200":{"description":"Up to 5 US address suggestions. Degrades to an empty list rather than an error when no key is configured (`enabled: false`), when `q` is under 3 or over 120 characters, or when the upstream geocoder fails.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutocompleteResponse"},"example":{"enabled":true,"suggestions":[{"label":"742 Evergreen Terrace, Springfield, IL 62704","line1":"742 Evergreen Terrace","city":"Springfield","state":"IL","zip":"62704","lat":39.78,"lon":-89.65}]}}}},"429":{"description":"Rate limited (30/minute per IP). Unlike the degraded cases above, this is the standard error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","example":60}}}}}}},"/v1/address/reverse":{"get":{"operationId":"reverseGeocode","summary":"Nearest mailable address for a coordinate","description":"Backs the one-tap \"use my location\" control in the send flow.","tags":["Addresses"],"parameters":[{"name":"lat","in":"query","required":true,"description":"Latitude, -90 to 90.","schema":{"type":"number","minimum":-90,"maximum":90}},{"name":"lon","in":"query","required":true,"description":"Longitude, -180 to 180.","schema":{"type":"number","minimum":-180,"maximum":180}}],"responses":{"200":{"description":"Nearest address, or null when nothing matched","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReverseGeocodeResponse"},"example":{"enabled":true,"address":{"label":"742 Evergreen Terrace, Springfield, IL 62704","line1":"742 Evergreen Terrace","city":"Springfield","state":"IL","zip":"62704","lat":39.78,"lon":-89.65}}}}},"400":{"description":"Coordinates missing or out of range. Returns a null address rather than an error envelope.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReverseGeocodeResponse"},"example":{"enabled":true,"address":null}}}}}}},"/v1/stats":{"get":{"operationId":"getStats","summary":"Public site totals","description":"Currently just the running count of letters mailed (sandbox and live), the number the homepage footer counter reads.","tags":["Meta"],"responses":{"200":{"description":"Site totals","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsResponse"},"example":{"status":"ok","letters_mailed":128}}}}}}},"/v1/openapi.json":{"get":{"operationId":"getOpenApiDocument","summary":"This document","description":"Generated from the zod schemas at request time, so it always describes the code that is actually deployed.","tags":["Meta"],"responses":{"200":{"description":"An OpenAPI 3.1 document","content":{"application/json":{"schema":{"type":"object"}}}}}}}},"components":{"schemas":{"Address":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":80},"line1":{"type":"string","minLength":1,"maxLength":120},"line2":{"type":"string","maxLength":120},"city":{"type":"string","minLength":1,"maxLength":80},"state":{"type":"string","minLength":2,"maxLength":2},"zip":{"type":"string","pattern":"^\\d{5}(-\\d{4})?$"},"country":{"type":"string","const":"US"}},"required":["name","line1","city","state","zip"]},"AddressSuggestion":{"type":"object","properties":{"label":{"type":"string"},"line1":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"},"lat":{"type":"number"},"lon":{"type":"number"}},"required":["label","line1","city","state","zip","lat","lon"]},"MailClass":{"type":"string","enum":["first_class","certified","certified_err","priority"]},"Error":{"type":"object","properties":{"status":{"type":"string","enum":["failed","action_required"]},"code":{"type":"string"},"reason":{"type":"string"},"next":{"type":"array","items":{"type":"string"}}},"required":["status","code","reason","next"]},"QuoteRequest":{"type":"object","properties":{"mail_class":{"$ref":"#/components/schemas/MailClass"},"page_count":{"type":"integer","minimum":1,"maximum":12},"color":{"type":"boolean"},"tracking":{"type":"boolean"},"to":{"$ref":"#/components/schemas/Address"},"text":{"type":"string","maxLength":50000},"upload_key":{"type":"string","pattern":"^up_[0-9a-f]{32}\\.pdf$"},"pdf_url":{"type":"string","format":"uri"}},"required":["mail_class","page_count"]},"QuoteResponse":{"type":"object","properties":{"status":{"type":"string","const":"ok"},"total_cents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"total":{"type":"string"},"breakdown":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"amount_cents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["id","label","amount_cents"]}},"note":{"type":"string"},"confirmation_token":{"type":"string"},"expires_in_minutes":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"next":{"type":"array","items":{"type":"string"}}},"required":["status","total_cents","total","breakdown","note"]},"CreateOrder":{"type":"object","properties":{"mail_class":{"$ref":"#/components/schemas/MailClass"},"color":{"type":"boolean"},"tracking":{"type":"boolean"},"to":{"$ref":"#/components/schemas/Address"},"from":{"$ref":"#/components/schemas/Address"},"text":{"type":"string","maxLength":50000},"font":{"type":"string","maxLength":40},"upload_key":{"type":"string","pattern":"^up_[0-9a-f]{32}\\.pdf$"},"pdf_base64":{"type":"string","maxLength":14000000},"pdf_url":{"type":"string","format":"uri"},"email":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"credit_code":{"type":"string","pattern":"^pp-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$"},"account_session":{"type":"string","maxLength":2000},"webhook_url":{"type":"string","format":"uri"},"sandbox":{"type":"boolean"},"format":{"default":"letter","description":"The physical piece to mail. Defaults to 'letter'. Postcards (postcard_4x6 $1.99, postcard_6x9 $2.99, postcard_6x11 $3.49) and the notecard (`notecard` $5.99 — a folded card, enclosed like a greeting card rather than open-face like a postcard) are message-only today: `text` prints on the piece itself via a handwriting engine, no custom photo or artwork side, and no PDF/upload path; printing and First-Class postage are both included in the price. All of them allow only mail_class='first_class' and carry no tracking.","type":"string","enum":["letter","letter_windowed","notecard","postcard_4x6","postcard_6x9","postcard_6x11"]},"remove_branding":{"description":"Pay to leave the \"Mailed via paperplane\" mark off this envelope. Only charged where a mark would otherwise have been printed — never on Certified, Priority, or postcards, which carry no mark to begin with.","type":"boolean"},"confirmation_token":{"type":"string","maxLength":2000},"checkout_ui_mode":{"type":"string","enum":["hosted","embedded"]}},"required":["mail_class","to","from"]},"Order":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","enum":["draft","pending_payment","screening","held_for_review","submitted","mailed","delivered","refused","canceled","failed"]},"sandbox":{"type":"boolean"},"mail_class":{"$ref":"#/components/schemas/MailClass"},"page_count":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"color":{"type":"boolean"},"to":{"type":"object","properties":{"name":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"zip":{"type":"string"}},"required":["name","city","state","zip"]},"price_cents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"breakdown":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"amount_cents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["id","label","amount_cents"]}},"tracking_number":{"anyOf":[{"type":"string"},{"type":"null"}]},"expected_delivery_date":{"anyOf":[{"type":"string"},{"type":"null"}]},"refusal_reason":{"anyOf":[{"type":"string"},{"type":"null"}]},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","status","sandbox","mail_class","page_count","color","to","price_cents","breakdown","tracking_number","expected_delivery_date","refusal_reason","created_at","updated_at"]},"CreateOrderResponse":{"type":"object","properties":{"status":{"type":"string","enum":["ok","action_required","failed"]},"reason":{"type":"string"},"next":{"type":"array","items":{"type":"string"}},"order":{"$ref":"#/components/schemas/Order"},"corrected_to":{"$ref":"#/components/schemas/Address"},"capability":{"$ref":"#/components/schemas/CapabilityTokens"},"replayed":{"type":"boolean"},"checkout_ui_mode":{"type":"string","enum":["hosted","embedded"]},"payment_url":{"type":"string","format":"uri"},"payment_client_secret":{"type":"string"}},"required":["status","order"],"oneOf":[{"$ref":"#/components/schemas/OrderSettledResponse"},{"$ref":"#/components/schemas/OrderHostedCheckoutResponse"},{"$ref":"#/components/schemas/OrderEmbeddedCheckoutResponse"}]},"OrderSettledResponse":{"type":"object","properties":{"status":{"type":"string","enum":["ok","action_required","failed"]},"reason":{"type":"string"},"next":{"type":"array","items":{"type":"string"}},"order":{"$ref":"#/components/schemas/Order"},"corrected_to":{"$ref":"#/components/schemas/Address"},"capability":{"$ref":"#/components/schemas/CapabilityTokens"},"replayed":{"type":"boolean"}},"required":["status","order"]},"OrderHostedCheckoutResponse":{"type":"object","properties":{"status":{"type":"string","const":"action_required"},"checkout_ui_mode":{"type":"string","const":"hosted"},"payment_url":{"type":"string","format":"uri"},"reason":{"type":"string"},"next":{"type":"array","items":{"type":"string"}},"order":{"$ref":"#/components/schemas/Order"},"corrected_to":{"$ref":"#/components/schemas/Address"},"capability":{"$ref":"#/components/schemas/CapabilityTokens"},"replayed":{"type":"boolean"}},"required":["status","checkout_ui_mode","payment_url","reason","next","order"]},"OrderEmbeddedCheckoutResponse":{"type":"object","properties":{"status":{"type":"string","const":"action_required"},"checkout_ui_mode":{"type":"string","const":"embedded"},"payment_client_secret":{"type":"string"},"reason":{"type":"string"},"next":{"type":"array","items":{"type":"string"}},"order":{"$ref":"#/components/schemas/Order"},"corrected_to":{"$ref":"#/components/schemas/Address"},"capability":{"$ref":"#/components/schemas/CapabilityTokens"},"replayed":{"type":"boolean"}},"required":["status","checkout_ui_mode","payment_client_secret","reason","next","order"]},"CapabilityTokens":{"type":"object","properties":{"cancel_token":{"type":"string"},"review_token":{"type":"string"}},"required":["cancel_token","review_token"]},"OrderResponse":{"type":"object","properties":{"status":{"type":"string","const":"ok"},"order":{"$ref":"#/components/schemas/Order"}},"required":["status","order"]},"CreditBalanceResponse":{"type":"object","properties":{"status":{"type":"string","const":"ok"},"code":{"type":"string"},"balance_cents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"face_cents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"bonus_cents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"bonus_expires_at":{"type":"string"},"face_expires_at":{"type":"string"}},"required":["status","code","balance_cents","face_cents","bonus_cents","bonus_expires_at","face_expires_at"]},"CreditCheckoutRequest":{"type":"object","properties":{"pack":{"type":"string"},"email":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"checkout_ui_mode":{"type":"string","enum":["hosted","embedded"]}},"required":["pack","email"]},"CreditCheckoutResponse":{"type":"object","properties":{"status":{"type":"string","const":"ok"},"checkout_ui_mode":{"type":"string","enum":["hosted","embedded"]},"payment_url":{"type":"string","format":"uri"},"payment_client_secret":{"type":"string"}},"required":["status","checkout_ui_mode"],"oneOf":[{"$ref":"#/components/schemas/HostedCreditCheckoutResponse"},{"$ref":"#/components/schemas/EmbeddedCreditCheckoutResponse"}]},"HostedCreditCheckoutResponse":{"type":"object","properties":{"status":{"type":"string","const":"ok"},"checkout_ui_mode":{"type":"string","const":"hosted"},"payment_url":{"type":"string","format":"uri"}},"required":["status","checkout_ui_mode","payment_url"]},"EmbeddedCreditCheckoutResponse":{"type":"object","properties":{"status":{"type":"string","const":"ok"},"checkout_ui_mode":{"type":"string","const":"embedded"},"payment_client_secret":{"type":"string"}},"required":["status","checkout_ui_mode","payment_client_secret"]},"X402CreditsRequest":{"type":"object","properties":{"pack":{"type":"string"},"email":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"sandbox":{"type":"boolean"}},"required":["pack","email"]},"X402CreditsResponse":{"type":"object","properties":{"status":{"type":"string","const":"ok"},"credit_code":{"type":"string"},"face_cents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"bonus_cents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"settlement":{"type":"object","properties":{"network":{"type":"string"},"tx_hash":{"type":"string"},"stripe_payment_intent_id":{"type":"string"}},"required":["network","tx_hash"]},"replayed":{"type":"boolean"},"sandbox":{"type":"boolean"}},"required":["status","credit_code","face_cents","bonus_cents"]},"CreateReview":{"type":"object","properties":{"order_id":{"type":"string","minLength":1,"maxLength":60},"stars":{"type":"number"},"comment":{"type":"string","maxLength":500},"capability_token":{"type":"string"}},"required":["order_id","stars"]},"Review":{"type":"object","properties":{"id":{"type":"string"},"stars":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"comment":{"anyOf":[{"type":"string"},{"type":"null"}]},"name":{"type":"string"},"city":{"type":"string"},"state":{"type":"string"},"category":{"type":"string"},"sample":{"type":"boolean"},"created_at":{"type":"string"}},"required":["id","stars","comment","name","city","state","category","created_at"]},"ReviewResponse":{"type":"object","properties":{"status":{"type":"string","const":"ok"},"review":{"$ref":"#/components/schemas/Review"}},"required":["status","review"]},"ReviewListResponse":{"type":"object","properties":{"status":{"type":"string","const":"ok"},"reviews":{"type":"array","items":{"$ref":"#/components/schemas/Review"}}},"required":["status","reviews"]},"SuggestClassRequest":{"type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":50000}},"required":["text"]},"SuggestClassResponse":{"type":"object","properties":{"status":{"type":"string","const":"ok"},"suggested_class":{"$ref":"#/components/schemas/MailClass"},"reason":{"type":"string"},"source":{"type":"string","enum":["heuristic","llm"]}},"required":["status","suggested_class","reason","source"]},"AutocompleteResponse":{"type":"object","properties":{"enabled":{"type":"boolean"},"suggestions":{"type":"array","items":{"$ref":"#/components/schemas/AddressSuggestion"}}},"required":["enabled","suggestions"]},"ReverseGeocodeResponse":{"type":"object","properties":{"enabled":{"type":"boolean"},"address":{"anyOf":[{"$ref":"#/components/schemas/AddressSuggestion"},{"type":"null"}]}},"required":["enabled","address"]},"CsvMapRequest":{"type":"object","properties":{"headers":{"minItems":1,"maxItems":100,"type":"array","items":{"type":"string","maxLength":200}},"samples":{"maxItems":3,"type":"array","items":{"maxItems":100,"type":"array","items":{"type":"string","maxLength":500}}}},"required":["headers"]},"CsvMapResponse":{"type":"object","properties":{"status":{"type":"string","const":"ok"},"mapping":{"type":"object","propertyNames":{"type":"string","enum":["name","line1","line2","city","state","zip"]},"additionalProperties":{"type":"array","items":{"type":"string"}}},"source":{"type":"string","enum":["heuristic","llm"]}},"required":["status","mapping","source"]},"UploadTicketResponse":{"type":"object","properties":{"status":{"type":"string","const":"ok"},"upload_key":{"type":"string"},"upload_url":{"type":"string"},"method":{"type":"string","const":"PUT"},"headers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"expires_in":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"max_bytes":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"next":{"type":"array","items":{"type":"string"}}},"required":["status","upload_key","upload_url","method","headers","expires_in","max_bytes","next"]},"StatsResponse":{"type":"object","properties":{"status":{"type":"string","const":"ok"},"letters_mailed":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["status","letters_mailed"]}},"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Optional developer API key: `Authorization: Bearer pp_live_…` (or `pp_test_…` for a sandbox-only key). Omitting it is a supported way to call every route. A key that is sent and does not verify is refused with 401 rather than treated as anonymous, so a typo fails loudly instead of quietly losing the restriction you meant to apply. Enabled per deployment via `SCOPED_API_KEYS`."}}}}