> ## 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 a letter

> 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`.

Send 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.

On the agent path a `confirmation_token` from `POST /v1/quotes` is required; without one the call fails `428 confirmation_required` rather than sending.



## OpenAPI

````yaml /openapi.json post /v1/orders
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.


    Every failure returns the same envelope: `status`, a stable machine-readable
    `code`, a human `reason`, and a `next` array of concrete recovery steps.


    This 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://sendpaperplane.com/contact
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
  termsOfService: https://sendpaperplane.com/terms
servers:
  - url: https://sendpaperplane.com
    description: Production
security: []
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/orders:
    post:
      tags:
        - Mail
      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`.


        Send 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.


        On the agent path a `confirmation_token` from `POST /v1/quotes` is
        required; without one the call fails `428 confirmation_required` rather
        than sending.
      operationId: createOrder
      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,

                This is a letter sent through paperplane.

                Best,
                Alex
              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'
      security:
        - {}
        - bearerAuth: []
components:
  schemas:
    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
    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'
    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
    MailClass:
      type: string
      enum:
        - first_class
        - certified
        - certified_err
        - priority
    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
    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
    CapabilityTokens:
      type: object
      properties:
        cancel_token:
          type: string
        review_token:
          type: string
      required:
        - cancel_token
        - review_token
    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
  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`.

````