---
title: PostalForm Agents Guide
description: PostalForm lets agents place a real print-and-mail order on behalf of their owner. Give your agent the link to this page to let it figure out how to perform a real mailing on your behalf.
seotitle: "PostalForm agents guide: machine payments (x402 and MPP)"
seo-description: Let an agent place a real print-and-mail order or bulk mailing campaign on behalf of its owner by using either x402 or the MPP endpoint and tracking fulfillment.
group: resources
indexable: true
nav: true
schema: webpage
eyebrow: Agents
published: 2026-02-15T12:00:00-06:00
updated: 2026-09-30T00:00:00+00:00
path: /agents
---
# PostalForm Agents Guide

PostalForm lets agents place a real print-and-mail order on behalf of their owner. Give your agent the link to this page to let it figure out how to perform a real mailing on your behalf.

## When the agent has no compatible payment wallet
PDF, letter, form, postcard and ordinary bulk machine orders return a signed `checkout_url`, `status_url`, `payment_options`, and `next_action` alongside the 402 challenge. MPP draft tools also return `checkout_url`. The document, order ID, page count and total stay the same whichever payment option the buyer chooses. Hosted checkout is an alternative to MPP/x402, not an MPP credential type. This fallback does not apply to the separate flower or shipping-label APIs.

1. Review `preview_url` (or the bulk `campaign_url`), recipients and total with the buyer. Preserve an uploaded document; do not recreate its contents as a letter to work around an upload problem.
2. If the runtime can actually obtain an approved compatible credential, answer one machine challenge. A connected Link account alone does not establish that the runtime can mint an SPT. Link supports distinct virtual-card, SPT and Link Pay Token integrations; use the capability the runtime exposes.
3. Otherwise present the returned `checkout_url`. Opening it only reviews the order. Continuing opens Stripe-hosted Checkout and reserves hosted payment for this order; subsequent machine calls return the same checkout option. No wallet setup or CLI installation is required for the buyer.
4. Poll `status_url` or `postalform.get_order_status`. `payment_authorized` means the card was authorized even while `is_paid` is false. `payment_processing` and `settled_pending_webhook` also mean wait, not pay again. `payment_review_required` needs support reconciliation; `closed` cannot be paid. Use fulfillment/tracking fields to distinguish printing, mailed and delivered.

Checkout links expire after 24 hours; poll the same order for a fresh link. Concurrent checkout starts reuse a session. A credential rejected before payment (for example, a malformed proof or expired challenge) leaves the same order payable with a corrected credential or hosted checkout. After an interrupted machine payment, retry the same credential and request or poll: a different credential or payment path is blocked while the outcome is uncertain. A 402 response alone does not prove that an earlier attempt failed; a later rejection does not unlock an uncertain payment. Follow the returned payment options and status. A late payment arriving outside the selected checkout is retained for reconciliation and does not trigger another mailing.

No native MCP connection? Use the HTTP create and status endpoints in [OpenAPI](/openapi.json). Do not switch to browser upload merely because the runtime cannot attach a remote MCP server. See [Stripe’s Link integration documentation](https://github.com/stripe/link-cli#integrating-into-your-agent) for agent-owned approval and credential retrieval; PostalForm does not mint credentials from a user’s Link account.

## Remote MCP connector details
For the customer-facing ChatGPT mailing plugin, use the separate guest endpoint `https://postalform.com/chatgpt/mcp`. It protects each draft with a private, expiring token from `postalform.open_mailroom`, requires that token on subsequent calls, presents a printable review and current USD price, and opens hosted checkout for the customer to pay. It supports one recipient with sender and recipient countries from the same 12-country checkout list; available mail services and prices depend on the destination. It does not expose machine-payment tools. See the [ChatGPT customer plugin guide](/developers#chatgpt-customer-plugin). The discovery details below describe the general-purpose `/mcp` endpoint.

Use these details when adding PostalForm to an MCP client, connector directory, or agent registry:

- Remote MCP endpoint: `https://postalform.com/mcp`
- Transport: streamable HTTP
- A2A Agent Card: `https://postalform.com/.well-known/agent-card.json`
- A2A JSON-RPC discovery bridge: `https://postalform.com/a2a`
- Server card: `https://postalform.com/.well-known/mcp/server-card.json`
- Compatibility manifest: `https://postalform.com/.well-known/mcp.json`
- MCP Registry server manifest: `https://postalform.com/.well-known/mcp/server.json`
- Agent skill: `https://postalform.com/skill.md`
- LLM overview: `https://postalform.com/llms.txt`
- Full LLM content dump: `https://postalform.com/llms-full.txt`
- Open Knowledge Format index: `https://postalform.com/okf/index.md`
- Developer docs: `https://postalform.com/developers`
- MPP-only endpoint guide: `https://postalform.com/mpp.md`
- MPP mail endpoint guide: `https://postalform.com/mpp-mail.md`
- MPP flower endpoint guide: `https://postalform.com/mpp-flowers.md`
- MPP shipping-label endpoint guide: `https://postalform.com/mpp-shipping-labels.md`

Prepare a draft for buyer review, then use `complete_checkout` with a compatible Stripe shared payment token after approval, or present the hosted `checkout_url`. Clients that can answer MPP or x402 challenges can use the direct machine-order tools with explicit owner approval and a spend limit. Choose by payment capability rather than client name.

For the product architecture behind this default, see [making physical mail callable from AI agents](/making-physical-mail-callable-from-ai-agents).
For a shorter non-branded overview aimed at agent and MCP directories, see [send physical mail from an AI agent](/send-physical-mail-from-ai-agent).

## Client setup notes
PostalForm works with MCP-capable clients and runtimes that can reach a public streamable HTTP server, including ChatGPT custom connectors or OpenAI Responses API MCP tools, Gemini SDKs, Gemini CLI, Claude, Claude Code, Codex, Cursor, Windsurf, Cline, Replit, OpenClaw, Hermes, n8n, LangChain, and similar clients.

ChatGPT and OpenAI API integrations should configure a remote MCP server with `server_url` set to `https://postalform.com/mcp` and require approval before sharing user documents, addresses, or payment-related data. Gemini CLI can add PostalForm with:

```bash
gemini mcp add --transport http postalform https://postalform.com/mcp
```

Gemini SDK integrations can connect an MCP client session to the same endpoint and pass that session into Gemini's MCP/tool-calling integration. Drafts support hosted checkout or buyer-approved Stripe token checkout; verify the client's payment and attachment handoffs before relying on them.

## Machine payments overview
### x402

x402 machine payments use an HTTP `402` handshake:

1. Optional (recommended): your agent sends `POST /api/machine/orders/validate` to get a quote and validation hints before payment.
2. Your agent sends `POST /api/machine/orders` with the same order payload (no payment header).
3. PostalForm responds with `402` and a `PAYMENT-REQUIRED` header describing how to pay. The JSON body repeats the decoded challenge as `payment.payment_required`, for clients that can only read response bodies. Browser clients can read the header too: `/api/machine/*` allows cross-origin requests and exposes the payment headers.
4. Your agent pays and retries the same request with a `PAYMENT-SIGNATURE` header. PostalForm implements x402 v2; the v1 `X-PAYMENT` header is not accepted.
5. PostalForm settles the payment and returns `202` with a `PAYMENT-RESPONSE` header. The response may be `settled_pending_webhook` while Stripe verifies the Base transaction; poll status and do not pay again.

### Endpoints

- Validate and quote (no payment side effects): `POST https://postalform.com/api/machine/orders/validate`
- Create and pay: `POST https://postalform.com/api/machine/orders`
- Poll status: `GET https://postalform.com/api/machine/orders/:id`

### Networks and assets

- Live deployments typically settle with **USDC on Base**.
- Test environments may use testnets (for example Base Sepolia).

### MPP endpoint

The MPP machine path also uses an HTTP `402` flow, but with the MPP headers and receipt format:

1. Optional (recommended): your agent sends `POST /api/machine/mpp/orders/validate` to get a quote and validation hints before payment.
2. Your agent sends `POST /api/machine/mpp/orders` with the same order payload and no payment credential.
3. PostalForm responds with `402` and `WWW-Authenticate: Payment ...`.
4. Your agent pays and retries the same request with `Authorization: Payment ...`.
5. PostalForm verifies the payment and returns `202` with a `Payment-Receipt` header while the order waits for Stripe webhook finalization.

### Endpoints

- Validate and quote (no payment side effects): `POST https://postalform.com/api/machine/mpp/orders/validate`
- Create and pay: `POST https://postalform.com/api/machine/mpp/orders`
- Poll status: `GET https://postalform.com/api/machine/mpp/orders/:id`

## Discovery
PostalForm exposes machine-payment discovery in two layers:

- Canonical discovery: `GET https://postalform.com/openapi.json`
- A2A Agent Card: `GET https://postalform.com/.well-known/agent-card.json`
- x402 manifest: `GET https://postalform.com/.well-known/x402`
- x402 manifest alias: `GET https://postalform.com/.well-known/x402.json`
- OKF concept index: `GET https://postalform.com/okf/index.md`
- Workflow forms catalog: `GET https://postalform.com/api/machine/forms`
- Workflow form schema: `GET https://postalform.com/api/machine/forms/{slug}/schema`
- Flower categories: `GET https://postalform.com/api/flowers/categories`
- Flower products: `GET https://postalform.com/api/flowers/products?category=bs&count=18&start=1&sorttype=pa`
- Flower delivery dates: `GET https://postalform.com/api/flowers/delivery-dates?zipcode=32503`

Use the OpenAPI document by default. It includes per-route input schemas, payable route metadata (`x-payment-info`), high-level guidance in `info.description`, and `x-agent-guidance` for agent planning.

Use the workflow form endpoints when the agent needs PostalForm to fill a known form PDF or assemble an upload-first document packet from structured JSON. The catalog lists published workflows that are safe for the single-mailpiece machine-order path; statutory workflows requiring coordinated service on multiple recipients are intentionally excluded and must use their PostalForm web workflow. The schema endpoint returns fields, groups, dependencies, attachment requirements, and machine submission paths. Fill values by field id from that schema, include every required attachment by its schema id, then send a top-level `form` object to the same validate/create endpoints used for PDF-backed machine orders.

For `plain-letter` attachments sent directly in a machine form payload, use the attachment types in its schema: PDF, JPG/PNG, plain text, Markdown, HTML, or RTF. The website also accepts DOCX and other supported image formats and converts them to PDF in the browser before submitting the full packet for preview.

The `/.well-known/x402` manifest is a lightweight compatibility layer for x402 crawlers and payment-aware agent runtimes. It advertises the create route, pricing mode, network, token, facilitator URL, and the validate/status endpoints for the machine order flow.

For a shorter x402-specific overview with directory fields, endpoint tables, and safety defaults, see [x402 physical mail API](/x402-physical-mail-api).

For Visa CLI, MPP registry, or MPP-only merchant verification, use the focused [MPP endpoint guide](/mpp), [MPP mail endpoint guide](/mpp-mail), [MPP flower endpoint guide](/mpp-flowers), or [MPP shipping-label endpoint guide](/mpp-shipping-labels) instead of this broader x402/MCP agent guide.

Payment instruments:

- **Tempo crypto** for MPP-capable crypto clients. Use Tempo testnet in local/integration environments and Tempo mainnet in live environments once enabled for your account.
- **Stripe Shared Payment Tokens** (`spt_...`) for card/Link-capable agentic clients. Your agent must mint the `spt_...` token through Stripe; PostalForm does **not** accept raw card details over this API.
- **Card MPP** (`card`) for Visa/card-MPP clients that answer the card challenge with an encrypted MPP card credential. Local and test environments use the Visa SDK mock flow; production advertises card automatically when Visa Acceptance gateway credentials and the card decryption private key are configured.

If you want to use Stripe-backed cards under the MPP flow, use the [Stripe Link CLI](https://github.com/stripe/link-cli) to safely issue an SPT token.
If you are testing the Visa card-MPP path, choose the returned `card` challenge and answer it with an MPP card client such as `mpp-card/client`.

Very brief Link CLI setup:

1. Install the CLI (see the GitHub install instructions): `brew install stripe/link-cli/link` (macOS/Homebrew) or `npm i -g @stripe/link`.
2. Authenticate the CLI with your Stripe account: `link auth login`.
3. Use the CLI to mint an `spt_...` Shared Payment Token and pass it in the MPP payment flow.

Today the MPP path is best for agents that already speak MPP challenge/receipt semantics and can pay with Tempo, Stripe Shared Payment Tokens, or card-MPP credentials directly. If your agent does not already support MPP payment provisioning, start with x402 (`purl`) or the MCP draft + hosted checkout flow instead.

## Flower letters overview
Flower letters are a dedicated machine-payment product. Do **not** send them through the PDF, form, or bulk mail endpoints. Use the flower catalog endpoints to choose an arrangement, then use the dedicated flower-letter machine endpoints to quote, pay, and track the order.

Use this flow when an agent should send real flowers with a card note attached:

1. Choose the delivery ZIP from the final recipient address.
2. List categories with `GET https://postalform.com/api/flowers/categories`.
3. List arrangements with `GET https://postalform.com/api/flowers/products?category=bs&count=18&start=1&sorttype=pa`.
4. Select one product from `products`; use its `CODE` as `product_code`. Product responses include Florist One fields such as `CODE`, `NAME`, `PRICE`, `DESCRIPTION`, `SMALL`, and `LARGE`. Treat `PRICE` as the arrangement price before delivery and tax, not the final customer total.
5. Fetch available dates with `GET https://postalform.com/api/flowers/delivery-dates?zipcode=<recipient_zip>`.
6. Build a flower-letter JSON payload with the selected `product_code`, selected `delivery_date`, full sender and recipient details, and a `note` of 200 characters or fewer.
7. Validate and quote with `POST https://postalform.com/api/machine/mpp/flower-letters/validate`. The validate response is the first point where PostalForm has the destination ZIP, delivery date, product, and final Florist One total together.
8. Create the unpaid order with `POST https://postalform.com/api/machine/mpp/flower-letters` and no `Authorization` header.
9. Answer the returned `WWW-Authenticate: Payment ...` challenge using the same MPP process described below.
10. Retry the exact same `POST /api/machine/mpp/flower-letters` body with `Authorization: Payment ...`.
11. Read `Payment-Receipt`, then poll `GET https://postalform.com/api/machine/mpp/flower-letters/:id` until `payment_status` becomes `paid`.

Important flower-letter rules:

- `request_id` is the idempotency key. Reuse it with the exact same JSON body after `402`; generate a fresh UUID for a different flower delivery.
- `buyer_email` is required for machine payment receipts. `customer.email` is optional and defaults to `buyer_email`.
- `note` is printed on the florist card and must be 200 characters or fewer.
- `zipcode` must be the recipient delivery ZIP and must match `recipient.zipcode`.
- `customer` is the sender. `recipient` is the person or facility receiving the flowers.
- Both `customer.phone` and `recipient.phone` must resolve to 10 US digits.
- `country` may be omitted; PostalForm defaults it to `US`.
- `allow_substitutions` defaults to `true`.
- Do not present catalog `PRICE` as the amount due. Present it as "before delivery and tax" until the validate response returns `quote.total.orderTotalCents`.
- PostalForm validates the product, ZIP, selected delivery date, and total before issuing a payment challenge. The payment challenge description and response body include the same final ZIP-based price breakdown when available. PostalForm checks the delivery date again when submitting the paid order to Florist One.
- Machine flower totals are capped by PostalForm's configured flower machine-payment limit. If validation returns `amount_exceeds_limit`, choose a lower-priced arrangement.

### Flower-letter MPP example

```bash
curl -sS https://postalform.com/api/machine/mpp/flower-letters/validate \
  -H 'Content-Type: application/json' \
  --json '{
    "request_id": "8c1a1b58-2c8f-4f4f-9c46-2c1ac32d7a1b",
    "buyer_name": "Agent Owner",
    "buyer_email": "owner@example.com",
    "note": "For your desk, your day, and the smile I hope this brings.",
    "zipcode": "32503",
    "product_code": "F1-509",
    "delivery_date": "2026-07-15",
    "customer": {
      "name": "Agent Owner",
      "phone": "8505550100",
      "address1": "123 Sender St",
      "city": "Pensacola",
      "state": "FL",
      "zipcode": "32503"
    },
    "recipient": {
      "name": "Recipient Example",
      "phone": "8505550199",
      "address1": "456 Recipient Ave",
      "city": "Pensacola",
      "state": "FL",
      "zipcode": "32503"
    },
    "allow_substitutions": true
  }'
```

The validate response includes `protocol: "mpp"`, `methods` such as `["tempo", "stripe_spt", "card"]` when card-MPP is configured, `product_type: "flower_letter"`, and a `quote` with the selected product and final total in cents. Use `quote.product.priceCents` for the arrangement price before delivery and tax. Use `quote.total.orderTotalCents` as the customer-facing amount due, and show `quote.total.deliveryChargeCents` and `quote.total.taxTotalCents` as the breakdown when present.

Then create and pay using the same body:

```bash
curl -i https://postalform.com/api/machine/mpp/flower-letters \
  -H 'Content-Type: application/json' \
  --json @flower-letter-body.json
```

On `402`, parse the `WWW-Authenticate: Payment ...` headers, choose exactly one supported MPP method, serialize the full MPP credential, and retry:

```bash
curl -i https://postalform.com/api/machine/mpp/flower-letters \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Payment <serialized-mpp-credential>' \
  --json @flower-letter-body.json
```

For x402 clients, use the same flower-letter JSON body with:

- Validate and quote: `POST https://postalform.com/api/machine/flower-letters/validate`
- Create and pay: `POST https://postalform.com/api/machine/flower-letters`
- Poll status: `GET https://postalform.com/api/machine/flower-letters/:id`

## Shipping labels overview
Shipping labels are a dedicated MPP-only product. Do not send parcel data through the document-mail, flower, x402, hosted-checkout, or MCP draft endpoints.

1. Send U.S. `from` and `to` addresses plus actual packaged weight and exterior dimensions to `POST https://postalform.com/api/machine/mpp/shipping-labels/validate`.
2. Present the returned carrier/service options and all-in totals. Omit `carrier` and `service` to use the lowest returned rate, or select an exact pair from `available_rates`.
3. Create the unpaid order at `POST https://postalform.com/api/machine/mpp/shipping-labels`.
4. Answer one returned MPP challenge and retry the exact request body and `request_id`.
5. Download the signed PDF from `label_download_url`, use the email sent to `buyer_email`, or open `order_complete_url`.
6. Poll `GET https://postalform.com/api/machine/mpp/shipping-labels/:id` if payment or carrier purchase is pending.

Supported carriers, services, weight limits, and rate availability come from the live carrier quote for the specific route and parcel. PostalForm supplies the label PDF but does not package or tender the parcel. See the [MPP shipping-label guide](/mpp-shipping-labels) for the complete schema and pricing fields.

## Bulk mailing overview
### Bulk through MCP

Provide exactly one of `bulk.csv_content` or `bulk.recipients`. Both use the same quote, review, payment, and fulfillment flow; JSON needs no CSV file or client-side conversion.

Call `postalform.create_machine_order` with the same `bulk` object described below and `payment_protocol: "mpp"` (default) or `"x402"`. Omit all top-level recipient fields, `letter`, and `form`. Bulk supports letters in envelopes; postcard options are not accepted. For `content_mode: "pdf"`, supply the shared PDF in the top-level `pdf` field, including an `upload_token` from `postalform.create_pdf_upload`. For `text` or `html`, supply the corresponding template inside `bulk` and omit `pdf`. `color` and `double_sided` apply to every recipient.

The unpaid result includes `campaign_url` for reviewing recipients and prepared documents, `bulk.recipient_count`, `bulk.content_mode`, and the combined `price_usd`. Bulk uses the campaign dashboard rather than a single PDF `preview_url`. Obtain approval for the entire campaign and total before paying.

- MPP: retry the same create arguments and `request_id` with `payment_authorization`, or call `postalform.pay_order` with just `order_id` to fetch that endpoint's challenge, then answer it using the same order ID and `payment_authorization`. When switching endpoints, fetch the new challenge first. Paying by order ID uses stored documents and does not require the original upload token to remain available.
- x402: retry the same create arguments and `request_id` with `payment_signature`.
- After payment, `postalform.get_order_status` returns `campaign_url` and `bulk.status_counts`; the dashboard shows individual recipient details. A campaign's payment or queue state does not establish delivery to every recipient.

### Bulk through the HTTP API

Bulk mailing uses the same validate, create, and poll endpoints as the single-recipient machine order flow. Send one sender address plus a `bulk` object containing either CSV text or a JSON recipient list and a content mode. Use this to send one document to many recipients or generate personalized documents from merge fields. Rendering and PDF sanitization happen server-side.

Required `bulk` fields:

- Exactly one of `csv_content` (raw CSV text) or `recipients` (a nonempty JSON array of address objects). Supplying both or neither is rejected.
- `content_mode`: one of `text`, `html`, or `pdf`

Optional `bulk` fields:

- `campaign_name`
- `template_text` (required when `content_mode="text"`)
- `template_html` (required when `content_mode="html"`)

Content modes:

- `text`: PostalForm merges `{{column_name}}` variables into plain text and renders one PDF letter per row. HTML tags in the template or merge values are rejected (`html_in_text_template`); use `html` mode for HTML letters.
- `html`: PostalForm merges variables into your HTML and renders one PDF letter per row. `{{column_name}}` inserts a value as escaped text. `{{{column_name}}}` (three braces) inserts it as HTML, so use it for per-recipient HTML bodies such as `{{{letter_body}}}`. A merge value that contains HTML but is referenced with two braces is rejected (`html_in_escaped_merge_field`), because its tags would print literally. Scripts and remote resources are not loaded; embed images as `data:` URLs.
- `pdf`: PostalForm reuses the top-level `pdf` for every row. There is no merge-field support in this mode.

CSV rules:

- Required columns: `line1`, `city`, `state`, `zip`
- Optional address columns: `line2`, `recipient_name`
- Any other columns become merge fields available to `template_text` or `template_html`

JSON recipient rules (MCP, MPP, and x402):

- Required: `line1`, `city`, and `zip` as strings. Keep ZIP/postal codes as strings to preserve leading zeros.
- `state` is required for US and Canadian recipients. Optional: `recipient_name`, `line2`, and `country` (defaults to `US`). Omit optional fields or send them as `null`. Supported countries and address validation are the same as CSV.
- Optional `merge_fields` maps lowercase snake_case names to strings, numbers, booleans, or null. Use `{{reference}}`, for example, for `merge_fields.reference`. Null becomes empty text. Address names, `full_address`, `constructor`, and `prototype` are reserved.
- Address fields such as `{{recipient_name}}` and `{{full_address}}` are also available to templates. Nested merge values and unknown recipient fields are rejected.
- List order and duplicate entries are preserved: each entry is a separate mailing. Reordering JSON object keys does not change the request identity. Keep the same input format and recipient list on payment retries.

Example `bulk` object, used with the usual buyer, sender, and `request_id` fields:

```json
{
  "content_mode": "text",
  "template_text": "Hello {{recipient_name}}, your reference is {{reference}}.",
  "recipients": [
    {
      "recipient_name": "Jane Doe",
      "line1": "123 Main St",
      "city": "Boston",
      "state": "MA",
      "zip": "02108",
      "country": "US",
      "merge_fields": { "reference": "INV-100" }
    }
  ]
}
```

Bulk requests still require one sender address, chosen the same way as single-recipient machine orders:

- Loqate sender: `sender_address_id`, `sender_address_type="Address"`, `sender_address_text`
- Manual sender: `sender_address_type="Manual"`, `sender_address_manual`

When `bulk` is present:

- do not send `recipient_name` or recipient address fields
- validation responses include `bulk.recipient_count` and `bulk.content_mode`
- create and poll responses may include `campaign_url`
- each CSV row or JSON recipient becomes its own mailed item with independent tracking and final delivery state on the campaign dashboard

## Address strategy (manual vs autocomplete)
The payload guidance below applies to both machine REST paths. The core order payload is the same for x402 and MPP.

Machine orders can now describe either:

- `mailpiece_type: "letter"` (default)
- `mailpiece_type: "postcard"` with `postcard_size: "4x6" | "6x9" | "11x6"`

For postcard orders, keep `pdf` exactly as you use it today, but treat it as the fully composed postcard PDF.

Postcard PDF requirements:

- Use a **2-page PDF**.
- **Page 1 is the artwork side**.
- **Page 2 is the mailing side**. You can place backside artwork or a non-address message there, but do **not** place sender/recipient names, return or delivery addresses, indicia, or barcode data in the PDF. PostalForm fills the mailing block automatically.
- The selected `postcard_size` must match the exact bleed template dimensions shown in the [postcard PDF guidelines](/postcard-pdf-guidelines).

For each party (sender + recipient), choose exactly one strategy:

- **Manual address**: best when you already have `line1`, `city`, `state`, and `zip`.
- **Autocomplete (Loqate ID)**: best when you have incomplete address info and want the agent to resolve the full address autonomously.

Rules:

- `countryCode` may be omitted for manual addresses; PostalForm defaults it to `US`.
- Loqate address IDs carry their country prefix (for example `US|...`, `CA|...`, `GB|...`).
- Manual country codes and Loqate ID prefixes must match a country offered by PostalForm's checkout address picker: `US`, `CA`, `AT`, `BE`, `CH`, `DE`, `ES`, `FR`, `GB`, `IN`, `LU`, or `NL`. Unsupported or invalid codes are rejected instead of defaulting to `US`.
- Do not send both a Loqate id and a manual address for the same party. Pick one and set `*_address_type` accordingly.
- There is no automatic fallback from Loqate verification to manual later in the pipeline. If you want manual, send manual.

## Option A: Manual address input (full address known)
If you already have the full address fields, you can skip autocomplete entirely and provide a **manual** address.

Manual addresses must include:

- `line1` (street address)
- `line2` (optional unit/suite)
- `city`
- `state` (required for US/Canada; use the state, province, or region where applicable)
- `zip` (ZIP, ZIP+4, or postal code)
- `countryCode` (optional two-letter code; defaults to `US`)

For bulk orders, add an optional `country`, `country_code`, or `countrycode` CSV column, or `country` on JSON recipient objects. Every recipient uses the same checkout-supported country list; a missing country defaults to `US`.

In the machine order payload:

- Set `sender_address_type: "Manual"` and include `sender_address_manual`.
- Set `recipient_address_type: "Manual"` and include `recipient_address_manual`.

Example manual address object:

```json
{
  "line1": "123 Main St",
  "line2": "Apt 4",
  "city": "Springfield",
  "state": "IL",
  "zip": "62701",
  "countryCode": "US"
}
```

## Find sender and recipient addresses (Loqate IDs)
If you don't have a complete address (or want a convenience/autofill flow), use **Loqate "Address" IDs**
for `sender_address_id` and `recipient_address_id`.

Address suggestions can include two types:

- `type="Address"`: a deliverable mailing address you can use directly.
- `type="Container"`: a building/complex (for example an apartment building). You must drill down to an `Address` (suite/unit) before placing an order.

Recommended agent flow (via MCP tool):

1. Call `postalform.search_addresses` with `query` (min 3 chars) and optional `country_code` (defaults to `US`).
2. If you see `type="Container"` results, ask for the unit/suite and call again with `container=<id>` and a refined `query`.
3. Select a suggestion where `type="Address"`. Use:

- `id` -> `*_address_id`
- `type` -> `*_address_type` (must be `"Address"` for Loqate-based verification)
- `${text}, ${description}` -> `*_address_text`

Example search:

```json
{
  "name": "postalform.search_addresses",
  "arguments": {
    "target": "recipient",
    "query": "1600 Amphitheatre Pkwy, Mountain View"
  }
}
```

Example drill-down (Container -> Address):

```json
{
  "name": "postalform.search_addresses",
  "arguments": {
    "target": "recipient",
    "query": "Apt 4",
    "container": "US|LP|Pz0_Qj4_bGJg|123456|99_ENG"
  }
}
```

Note:

- If you send a `Container` id into the order payload, Loqate verification will fail later and the order will not mail.
- If you already have a complete address (line1/city/state/zip/countryCode), you can use **manual address input** instead of Loqate IDs.

## Draft an order payload (x402)
Required fields:

- `request_id` (UUID, used for idempotency across one logical mailing attempt; reuse it for payment retries and generate a fresh one for a legitimate second order)
- `buyer_name`
- `buyer_email` (required; used as Stripe `receipt_email` so receipts are delivered to the buyer)
- exactly one document source:
  - `pdf` (recommended canonical format: `{ "upload_token": "..." }`; also accepts `{ download_url, file_id }`, a `data:application/pdf;base64,...` URL, or a public HTTPS URL), or
  - `letter` (a string or object that PostalForm renders server-side to PDF; optional `format` is `text`, `html`, `markdown`, or `rtf`, default `text`), or
  - `form` (a structured workflow form payload discovered through `GET /api/machine/forms`)
- Sender: `sender_name` and either:
  - Loqate: `sender_address_id`, `sender_address_type="Address"`, `sender_address_text`, or
  - Manual: `sender_address_type="Manual"`, `sender_address_manual`
- Recipient: `recipient_name` and either:
  - Loqate: `recipient_address_id`, `recipient_address_type="Address"`, `recipient_address_text`, or
  - Manual: `recipient_address_type="Manual"`, `recipient_address_manual`

Common options:

- `double_sided` (default `true`)
- `color` (default `false`)
- `mail_class` (`standard`, `priority`, `express`)
- `certified` (default `false`; proof mail: USPS Certified Mail for US destinations, or PinGen registered mail for supported European destinations such as CH/FR/BE; standard/First Class only)
- `certified_return_receipt` (default `false`; return-receipt add-on for U.S. Certified Mail only)
- `return_receipt_format` (`electronic` by default, or `physical` for mailed PS Form 3811)
- `restricted_delivery` (default `false`; requests addressee-only USPS Restricted Delivery)
- `err_delivery` (`manual` by default, or `email` for automatic signed-PDF delivery)
- `err_email` (optional automatic-delivery override; defaults to the buyer email)
- `signature_required` (default `false`; requests USPS signature-on-delivery; ignored for postcards or when `mail_class="standard"`)
- `mailpiece_type` (`letter` or `postcard`; default `letter`)
- `postcard_size` (`4x6`, `6x9`, `11x6`; required when `mailpiece_type="postcard"`)

For Certified Mail, set `certified=true` to request basic Certified Mail. Add `certified_return_receipt=true` when the sender also wants a return receipt. Electronic is the default and produces the USPS delivery record with the recipient signature image or approved hand-stamp; use `return_receipt_format="physical"` for the mailed green card. Set `restricted_delivery=true` for addressee-only delivery. With electronic receipts, `err_delivery="manual"` leaves retrieval to the sender through USPS tracking, while `email` makes PostalForm acquire and email the PDF. If `certified=false`, these return-receipt options are invalid.

Postcard behavior:

- Keep using the same endpoints and payment handshake.
- Provide `mailpiece_type: "postcard"` and a valid `postcard_size`.
- Use `pdf` as the final postcard PDF, not a template id or text-only draft.
- Do not typeset sender/recipient names or mailing addresses into the postcard PDF. Provide sender/recipient data in the order fields and PostalForm places the mailing data automatically.
- Lob-routed international postcards require `postcard_size="4x6"` and a U.S. sender/return address. PostalForm routes larger international postcards or international postcards with non-U.S. return addresses to PostGrid before provider submission.
- Follow the published [postcard PDF guidelines](/postcard-pdf-guidelines) and template downloads:
  - [4x6 template](/postcard-guidelines/us_intl_postcard_6inx4in.pdf)
  - [6x9 template](/postcard-guidelines/us_intl_postcard_9inx6in.pdf)
  - [11x6 template](/postcard-guidelines/us_intl_postcard_11inx6in.pdf)
- PostalForm normalizes postcard mail options server-side to:
  - `color=true`
  - `double_sided=true`
  - `mail_class="standard"`
  - `certified=false`
  - `certified_return_receipt=false`
  - `signature_required=false`

You can mix-and-match:

- Manual sender + Loqate recipient
- Loqate sender + manual recipient

Example request:

```json
{
  "request_id": "8c1a1b58-2c8f-4f4f-9c46-2c1ac32d7a1b",
  "buyer_name": "Agent Owner",
  "buyer_email": "owner@example.com",
  "pdf": {
    "download_url": "https://example.oaiusercontent.com/file.pdf",
    "file_id": "file_abc123"
  },
  "file_name": "letter.pdf",
  "sender_name": "Sender Example",
  "sender_address_id": "US|LP|Pz0_Qj4_bGJg|16074807|13_ENG",
  "sender_address_type": "Address",
  "sender_address_text": "123 Sender St, Springfield, IL 62701",
  "recipient_name": "Recipient Example",
  "recipient_address_id": "US|LP|Pz0_Qj4_bGJg|199825276|99_ENG",
  "recipient_address_type": "Address",
  "recipient_address_text": "456 Recipient Ave, Springfield, IL 62701",
  "double_sided": true,
  "color": false,
  "mail_class": "standard",
  "certified": false
}
```

Example request (server-rendered Markdown letter):

```json
{
  "request_id": "8c1a1b58-2c8f-4f4f-9c46-2c1ac32d7a1b",
  "buyer_name": "Agent Owner",
  "buyer_email": "owner@example.com",
  "letter": {
    "format": "markdown",
    "title": "Account notice",
    "body": "# Account notice\n\nPlease review the enclosed account update and contact us with any questions.",
    "signature": "Sender Example"
  },
  "file_name": "account-notice.pdf",
  "sender_name": "Sender Example",
  "sender_address_id": "US|LP|Pz0_Qj4_bGJg|16074807|13_ENG",
  "sender_address_type": "Address",
  "sender_address_text": "123 Sender St, Springfield, IL 62701",
  "recipient_name": "Recipient Example",
  "recipient_address_id": "US|LP|Pz0_Qj4_bGJg|199825276|99_ENG",
  "recipient_address_type": "Address",
  "recipient_address_text": "456 Recipient Ave, Springfield, IL 62701",
  "double_sided": true,
  "color": false,
  "mail_class": "standard",
  "certified": false
}
```

Example request (manual addresses):

```json
{
  "request_id": "8c1a1b58-2c8f-4f4f-9c46-2c1ac32d7a1b",
  "buyer_name": "Agent Owner",
  "buyer_email": "owner@example.com",
  "pdf": {
    "download_url": "https://example.oaiusercontent.com/file.pdf",
    "file_id": "file_abc123"
  },
  "file_name": "letter.pdf",
  "sender_name": "Sender Example",
  "sender_address_type": "Manual",
  "sender_address_manual": {
    "line1": "123 Sender St",
    "line2": "Apt 4",
    "city": "Springfield",
    "state": "IL",
    "zip": "62701"
  },
  "recipient_name": "Recipient Example",
  "recipient_address_type": "Manual",
  "recipient_address_manual": {
    "line1": "456 Recipient Ave",
    "line2": "",
    "city": "Springfield",
    "state": "IL",
    "zip": "62701"
  },
  "double_sided": true,
  "color": false,
  "mail_class": "standard",
  "certified": false
}
```

Example request (postcard):

```json
{
  "request_id": "8c1a1b58-2c8f-4f4f-9c46-2c1ac32d7a1b",
  "buyer_name": "Agent Owner",
  "buyer_email": "owner@example.com",
  "pdf": {
    "download_url": "https://example.oaiusercontent.com/postcard.pdf",
    "file_id": "file_postcard_123"
  },
  "file_name": "postcard.pdf",
  "mailpiece_type": "postcard",
  "postcard_size": "6x9",
  "sender_name": "Sender Example",
  "sender_address_id": "US|LP|Pz0_Qj4_bGJg|16074807|13_ENG",
  "sender_address_type": "Address",
  "sender_address_text": "123 Sender St, Springfield, IL 62701",
  "recipient_name": "Recipient Example",
  "recipient_address_id": "US|LP|Pz0_Qj4_bGJg|199825276|99_ENG",
  "recipient_address_type": "Address",
  "recipient_address_text": "456 Recipient Ave, Springfield, IL 62701"
}
```

Example request (workflow form):

```json
{
  "request_id": "8c1a1b58-2c8f-4f4f-9c46-2c1ac32d7a1b",
  "buyer_name": "Agent Owner",
  "buyer_email": "owner@example.com",
  "form": {
    "slug": "5471",
    "fields": {
      "foreign_corporation_period_begin": "01/01/2025",
      "foreign_corporation_period_end": "12/31/2025",
      "field_5": "Example Filer Inc.",
      "field_11": "12-3456789"
    }
  },
  "file_name": "Form-5471.pdf",
  "sender_name": "Sender Example",
  "sender_address_type": "Manual",
  "sender_address_manual": {
    "line1": "123 Sender St",
    "line2": "Apt 4",
    "city": "Springfield",
    "state": "IL",
    "zip": "62701"
  },
  "recipient_name": "Recipient Example",
  "recipient_address_type": "Manual",
  "recipient_address_manual": {
    "line1": "456 Recipient Ave",
    "line2": "",
    "city": "Springfield",
    "state": "IL",
    "zip": "62701"
  },
  "double_sided": true,
  "color": false,
  "mail_class": "standard",
  "certified": false
}
```

Workflow form rules:

- Discover first with `GET /api/machine/forms`, then fetch the selected schema with `GET /api/machine/forms/{slug}/schema`.
- Send `form.slug` and `form.fields`; field keys must come from the selected schema.
- For compound fields, send a nested object keyed by the compound subfield ids from the schema.
- If the schema lists required attachments, include them in `form.attachments` with `id` and `base64`; data URLs are accepted. Follow `allowed_types`, `max_files`, and `max_size_mb`, and keep all decoded inline attachments within `attachments_max_total_size_mb` (currently 12 MB combined).
- For workflows with `recipient.mode="user"`, the top-level `recipient_name` and recipient address create the envelope. Keep them aligned with any recipient address-role fields in `form.fields`; form-field prefill does not silently override a machine order's top-level envelope address.
- Do not send more than one of `pdf`, `letter`, or `form` in the same single-recipient order.
- Workflow forms, upload-first packets, and server-rendered letters are letter-only. Postcards still require a fully composed `pdf`.
- Validation renders and validates the filled workflow PDF server-side before the x402 or MPP payment challenge is issued.

Example request (bulk mailing with merge fields):

```json
{
  "request_id": "8c1a1b58-2c8f-4f4f-9c46-2c1ac32d7a1b",
  "buyer_name": "Agent Owner",
  "buyer_email": "owner@example.com",
  "sender_name": "Sender Example",
  "sender_address_id": "US|LP|Pz0_Qj4_bGJg|16074807|13_ENG",
  "sender_address_type": "Address",
  "sender_address_text": "123 Sender St, Springfield, IL 62701",
  "double_sided": true,
  "color": false,
  "mail_class": "standard",
  "certified": false,
  "bulk": {
    "campaign_name": "April Outreach",
    "csv_content": "recipient_name,line1,line2,city,state,zip,account_number\nJane Doe,123 Main St,,Springfield,IL,62701,ACCT-100\nJohn Smith,456 Oak Ave,Apt 2,Chicago,IL,60601,ACCT-200",
    "content_mode": "text",
    "template_text": "Hello {{recipient_name}}, your account reference is {{account_number}}."
  }
}
```

For HTML bulk mail, replace `content_mode` with `html` and send `template_html`. When every recipient gets a different HTML letter, send it in a merge field and reference it with three braces:

```json
{
  "content_mode": "html",
  "template_html": "{{{letter_body}}}",
  "recipients": [
    {
      "recipient_name": "Dr. Jane Doe",
      "line1": "123 Main St",
      "city": "Boston",
      "state": "MA",
      "zip": "02108",
      "merge_fields": { "letter_body": "<p>Dear Dr. Doe,</p><p>Thank you for your time.</p>" }
    }
  ]
}
```

For shared-PDF bulk mail, replace `content_mode` with `pdf` and provide the top-level `pdf` field. That same PDF will be mailed to every recipient row.

## Troubleshooting (common x402 API responses)
- `422` (`invalid_request`): payload validation failed. The response includes `errors[]` with `path`, `message`, and machine hints (`hint`, `fix_examples`) when available.
  - Manual addresses: US requires a valid two-letter state and ZIP/ZIP+4; Canada requires a province and Canadian postal code; other countries require `line1`, `city`, and postal code.
  - Address countries: manual `countryCode` values and Loqate ID country prefixes must be one of `US`, `CA`, `AT`, `BE`, `CH`, `DE`, `ES`, `FR`, `GB`, `IN`, `LU`, or `NL`.
  - Loqate addresses: `*_address_type` must be `"Address"` (not `"Container"`), and ids must be supported Loqate ids.
  - Don’t send `*_address_manual` unless `*_address_type="Manual"`.
  - Bulk mail: `bulk.csv_content` must parse into at least one valid row with `line1`, `city`, `state`, and `zip`; optional `country`, `country_code`, or `countrycode` values use the same checkout-supported country list.
  - Bulk text mode: `bulk.template_text` is required when `bulk.content_mode="text"`.
  - Bulk HTML mode: `bulk.template_html` is required when `bulk.content_mode="html"`.
  - Bulk shared-PDF mode: the top-level `pdf` is required when `bulk.content_mode="pdf"`.
  - Single-recipient document source: send exactly one of `pdf`, `letter`, or `form`.
  - If `mailpiece_type="postcard"`, you must send a valid `postcard_size`.
- `422` (`missing_buyer_email`): `buyer_email` was missing when attempting machine payment; this email is required to set Stripe `receipt_email`.
- `422` (`html_in_escaped_merge_field`): an HTML template inserts a merge value that contains HTML with `{{field}}`. Use `{{{field}}}`, or remove the markup from the value.
- `422` (`html_in_text_template`): a text template or its merge values contain HTML tags. Send `content_mode="html"` with `template_html` instead.
- `409` (`request_id_mismatch`): the same `request_id` was reused with a different payload. The response includes `status_url` for the existing order. Resend the original payload unchanged to continue that order, or send a new UUID to create a new one.
- `402` (`payment_required`): pay and retry the identical request with `PAYMENT-SIGNATURE` (or use `purl`). The body includes the decoded challenge as `payment.payment_required`, plus `checkout_url` for paying by card if you can't sign an x402 payment. A retry that included a payment says why it was not accepted in `message`. Single-recipient drafts may include `preview_url`, a signed short-lived PDF URL for reviewing the generated or uploaded draft before payment.
- `429` (`rate_limited`): slow down address searches and retry later.

## Pay autonomously (MPP endpoint)
Use the MPP path when your client already speaks MPP and can respond to `WWW-Authenticate: Payment ...` challenges.

The same MPP challenge flow applies to both single-recipient orders and bulk campaigns. For bulk mail, keep the same `bulk` object on validate, on the unpaid create call, and on the paid retry.

High-level flow:

1. Quote and validate with `POST https://postalform.com/api/machine/mpp/orders/validate`.
2. Expect a `200` response with `protocol: "mpp"` and `methods` such as `["tempo", "stripe_spt", "card"]` when card-MPP is configured.
3. Create the order with `POST https://postalform.com/api/machine/mpp/orders` and no payment credential.
4. Expect `402` with one or more `WWW-Authenticate: Payment ...` challenges. PostalForm returns one challenge per configured MPP method, for example `tempo`, `stripe`, and `card`.
5. Choose exactly one payment instrument and retry the **same** request body and `request_id` with `Authorization: Payment ...`.
6. Read the `Payment-Receipt` header from the success response. A paid request may still return `202 settled_pending_webhook` while Stripe finalizes the underlying PaymentIntent.
7. Poll `GET https://postalform.com/api/machine/mpp/orders/:id` until `payment_status` becomes `paid`.

### MPP payment required response

When PostalForm returns `402 payment_required` on the MPP endpoint:

- Do **not** create a new order.
- Reuse the same `request_id`.
- Reuse the exact same JSON request body.
- Keep the canonical `order_id` returned by PostalForm. A later `request_id` may alias back to that existing unpaid order.
- If `preview_url` is present, use it to review the generated or uploaded draft PDF before paying.
- Read the `WWW-Authenticate: Payment ...` headers and choose exactly one supported method.
- Retry the same `POST /api/machine/mpp/orders` call with `Authorization: Payment ...`.

### Choosing a payment instrument

- **Tempo**: use an MPP-capable Tempo client such as `mppx` to settle the Tempo challenge on chain.
- **Stripe SPT**: use your own Stripe integration to mint a `shared_payment.granted_token` (`spt_...`) from a customer-approved card or Link payment method, then answer the Stripe MPP challenge with that token.
- **Card MPP**: use an MPP card client such as `mpp-card/client` to answer the `card` challenge with an encrypted card credential. Local/test challenges can be paid with the SDK mock client; production challenges appear automatically when Visa Acceptance credentials and the card private key are configured.

#### Stripe SPT checklist

To pay the Stripe challenge successfully, your agent should:

1. Parse the `stripe` challenge from the `WWW-Authenticate` headers.
2. Read `amount`, `currency`, and `methodDetails.networkId` from that challenge.
3. Mint or obtain a buyer-approved granted token (`spt_...`) through Stripe for the **same** amount, the **same** currency, and the **same** seller `network_id`.
4. Serialize the chosen Stripe challenge plus `{ "spt": "..." }` into one MPP credential and send that full value back as the `Authorization` header.
5. Retry the exact same `POST /api/machine/mpp/orders` request body and the exact same `request_id`.

Important details:

- PostalForm does **not** accept a bare `spt_...` token by itself. The retry header must be the full serialized MPP credential (for example, `Credential.serialize(...)`), not just the token string.
- For Stripe challenges, `request.amount` is in the smallest currency unit (for USD, cents).
- In current PostalForm deployments the Stripe challenge commonly advertises `networkId: "internal"` and `paymentMethodTypes: ["card", "link"]`, but clients should trust the challenge payload instead of hard-coding those values.
- A Stripe SPT minted for the wrong `network_id`, amount, currency, or expiry window can fail even if the token string itself looks valid.

See [Stripe Shared Payment Tokens](https://docs.stripe.com/agentic-commerce/concepts/shared-payment-tokens) for the seller-side token lifecycle.

Important:

- Do **not** send raw card details (PAN/CVC/expiry) to PostalForm. PostalForm only accepts the resulting `Authorization: Payment ...` credential, which may embed an `spt_...` token for the Stripe path or an encrypted token payload for the card-MPP path.
- The first successful MPP create call returns order state plus a `Payment-Receipt`; the order may still remain in `settled_pending_webhook` briefly until the Stripe webhook marks it fully paid.

### Example: serialize a Stripe challenge into `Authorization`

If you already have an MPP-aware client but need the exact retry shape, the Stripe path looks like:

```ts
import { Challenge, Credential } from 'mppx'

const endpoint = 'https://postalform.com/api/machine/mpp/orders'
const body = {
  request_id: '8c1a1b58-2c8f-4f4f-9c46-2c1ac32d7a1b',
  buyer_name: 'Agent Owner',
  buyer_email: 'owner@example.com',
  sender_name: 'Sender Example',
  sender_address_type: 'Address',
  sender_address_id: 'US|LP|Pz0_Qj4_bGJg|16074807|13_ENG',
  sender_address_text: '123 Sender St, Springfield, IL 62701',
  bulk: {
    campaign_name: 'April Outreach',
    csv_content: 'recipient_name,line1,city,state,zip\\nJane Doe,123 Main St,Springfield,IL,62701',
    content_mode: 'text',
    template_text: 'Hello {{recipient_name}}.',
  },
}

const unpaid = await fetch(endpoint, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(body),
})

if (unpaid.status !== 402) {
  throw new Error(`Expected 402, received ${unpaid.status}`)
}

const challenges = Challenge.fromResponseList(unpaid)
const stripeChallenge = challenges.find((challenge) => challenge.method === 'stripe')
if (!stripeChallenge) {
  throw new Error('No stripe challenge returned')
}

const methodDetails = stripeChallenge.request.methodDetails as {
  networkId?: string
  paymentMethodTypes?: string[]
}

const spt = await mintSharedPaymentGrantedToken({
  amount: stripeChallenge.request.amount,
  currency: stripeChallenge.request.currency,
  networkId: methodDetails.networkId,
  externalId: body.request_id,
})

const authorization = Credential.serialize(
  Credential.from({
    challenge: stripeChallenge,
    payload: { spt },
  })
)

const paid = await fetch(endpoint, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': authorization,
  },
  body: JSON.stringify(body),
})
```

`mintSharedPaymentGrantedToken(...)` is your own Stripe-side token provisioning step. The critical part is that you reuse the challenge you just received, serialize that challenge plus `{ spt }`, and replay the **same** request body.

### Test mode: mint a Stripe SPT that matches the challenge

For sandbox or CI testing, Stripe's granted-token test helper can mint an `spt_...` token that matches the challenge:

```bash
curl https://api.stripe.com/v1/test_helpers/shared_payment/granted_tokens \
  -u "$STRIPE_SECRET_KEY:" \
  -H "Stripe-Version: 2026-03-04.preview" \
  -d payment_method=pm_card_visa \
  -d "usage_limits[currency]"=usd \
  -d "usage_limits[max_amount]"=340 \
  -d "usage_limits[expires_at]"=1775880017 \
  -d "seller_details[network_id]"=internal \
  -d "seller_details[external_id]"=order_mpp_1
```

Replace `usd`, `340`, `1775880017`, and `internal` with the values from the live Stripe challenge. In live mode, your agent should obtain a real buyer-approved `spt_...` through its own Stripe flow instead of the test helper.

### Example: Tempo with `mppx`

If your agent already uses `mppx`, a Tempo payment looks like:

```bash
mppx \
  https://postalform.com/api/machine/mpp/orders \
  --account my-tempo-wallet \
  --json-body '{"request_id":"8c1a1b58-2c8f-4f4f-9c46-2c1ac32d7a1b","buyer_name":"Agent Owner","buyer_email":"owner@example.com","pdf":{"download_url":"https://example.oaiusercontent.com/file.pdf","file_id":"file_abc123"},"sender_name":"Sender Example","sender_address_id":"US|LP|Pz0_Qj4_bGJg|16074807|13_ENG","sender_address_type":"Address","sender_address_text":"123 Sender St, Springfield, IL 62701","recipient_name":"Recipient Example","recipient_address_id":"US|LP|Pz0_Qj4_bGJg|199825276|99_ENG","recipient_address_type":"Address","recipient_address_text":"456 Recipient Ave, Springfield, IL 62701","double_sided":true,"color":false,"mail_class":"standard","certified":false}' \
  --include \
  --verbose
```

`mppx` will parse the `WWW-Authenticate: Payment ...` response, choose a compatible challenge, pay it, and retry the same request with `Authorization: Payment ...`.

### MPP-specific troubleshooting

- Retried request still returns `402`: you probably changed `request_id` or the JSON body, chose the wrong challenge, or sent a bare `spt_...` instead of a serialized credential.
- Stripe verification fails after you minted an SPT: ensure the granted token was minted for the challenge's exact `amount`, `currency`, and `methodDetails.networkId`.
- `202 settled_pending_webhook`: payment was accepted. Keep polling `GET /api/machine/mpp/orders/:id` until `payment_status` becomes `paid`; do **not** mint a second token or create a new order.
- Bulk campaigns follow the same rule set. A bulk MPP retry must reuse the identical `bulk` object and `request_id`.

## Pay autonomously (x402, recommended: purl)
Stripe maintains an agent-facing wallet CLI called [`purl`](https://github.com/stripe/purl) that can fully automate the `402` flow: it reads `PAYMENT-REQUIRED`, creates a payment, and retries the request with `PAYMENT-SIGNATURE`.

Install the CLI (requires Rust):

```bash
git clone https://github.com/stripe/purl
cd purl
cargo install --path cli
```

Example:

```bash
export POSTALFORM_URL="https://postalform.com/api/machine/orders"
export EVM_PRIVATE_KEY="0x..."

purl \
  --private-key "$EVM_PRIVATE_KEY" \
  --network eip155:8453 \
  --max-amount 5000000 \
  --output-format json \
  -X POST \
  --json '{"request_id":"8c1a1b58-2c8f-4f4f-9c46-2c1ac32d7a1b","buyer_name":"Agent Owner","buyer_email":"owner@example.com","pdf":{"download_url":"https://example.oaiusercontent.com/file.pdf","file_id":"file_abc123"},"sender_name":"Sender Example","sender_address_id":"US|LP|Pz0_Qj4_bGJg|16074807|13_ENG","sender_address_type":"Address","sender_address_text":"123 Sender St, Springfield, IL 62701","recipient_name":"Recipient Example","recipient_address_id":"US|LP|Pz0_Qj4_bGJg|199825276|99_ENG","recipient_address_type":"Address","recipient_address_text":"456 Recipient Ave, Springfield, IL 62701","double_sided":true,"color":false,"mail_class":"standard","certified":false}' \
  "$POSTALFORM_URL"
```

Notes:

- `--max-amount` is a safety cap in atomic units (USDC has 6 decimals). `purl` will refuse to pay if the server asks for more than this cap.
- `--network` must match the network returned in `PAYMENT-REQUIRED` (for Base mainnet use `eip155:8453`; for Base Sepolia use `eip155:84532`).
- Your agent can retry the same `request_id` safely. PostalForm rejects the same `request_id` if the payload changes.
- If you want to send the same document to the same addresses again after a prior order is paid or settled, generate a fresh `request_id`.

## Pay autonomously (x402, JavaScript)
If you prefer to implement the x402 handshake directly, use the official x402 packages:

- `@x402/core` for the HTTP protocol wrapper
- `@x402/evm` for EVM signing (USDC)

High-level flow:

1. `fetch(POST /api/machine/orders)` -> expect `402`.
2. Decode `PAYMENT-REQUIRED`.
3. Create a payment payload.
4. Retry request with `PAYMENT-SIGNATURE`.
5. Read `PAYMENT-RESPONSE` and persist the settlement tx hash.
6. If the response is `settled_pending_webhook`, poll the order status until `is_paid=true`; do not submit a second payment.

## Track fulfillment
Tracking depends on the payment path:

- x402: poll `GET https://postalform.com/api/machine/orders/:id`
- MPP endpoint: poll `GET https://postalform.com/api/machine/mpp/orders/:id`

For x402, after a successful payment, poll using either the canonical `order_id` returned by PostalForm or any aliased `request_id`:

```bash
curl -sS "https://postalform.com/api/machine/orders/<request_id>"
```

The response includes `is_paid`, `current_step`, x402/MPP settlement metadata, and the normalized `mailpiece_type` / `postcard_size` so an agent can confirm what was actually drafted.

It also includes `order_complete_url` with the following behavior:

- Before payment is settled: `"Order URL will be returned once payment is settled."`
- After payment is settled (`is_paid=true`): `https://postalform.com/order/complete?order_id=<request_id>`

For bulk mailing orders, the response may also include `campaign_url`, which links to the bulk campaign dashboard. Use that dashboard for per-recipient mailed-item statuses, delivery events, and tracking metadata.

## Tools for agents
If you need help finding address IDs or handling PDF uploads, the PostalForm MCP server can help:

- MCP endpoint: `https://postalform.com/mcp`
- Tools:
  - `postalform.search_addresses` (get `*_address_id` values)
  - `postalform.create_pdf_upload` (get an `upload_token` if you cannot pass a file download URL)
  - `postalform.list_forms` (list supported single-mailpiece workflow forms available to agents)
  - `postalform.get_form_schema` (fetch a supported workflow's fields, dependencies, attachments, and submission metadata)
  - `postalform.create_form_order_draft` (create a hosted-checkout draft from supported single-mailpiece workflow JSON)
  - `postalform.pay_order` (get an MPP challenge or pay a prepared MPP order using only its ID and buyer-approved credential)
  - `postalform.create_machine_order` (create a machine-paid PDF, letter, or workflow-form order; returns MPP/x402 challenges and accepts the paid retry credential)
  - `postalform.get_order_status` (read payment/processing state plus stored mailing status, carrier tracking number/status/events, and electronic return receipt availability for hosted draft/checkout flows; see /developers for field semantics. Missing tracking is not proof of non-delivery. Polling does not pay, mail, or acquire receipts.)

Use MCP draft tools for hosted checkout, a buyer-approved Stripe token payment through `complete_checkout`, or optional MPP via `payment_protocol: "mpp"` and `buyer_email`. MPP drafts are paid with `postalform.pay_order` using the existing order ID and credential, without resending the content. Retry an interrupted token checkout with the same session and token; processing or unverified payments must not prompt a second order or replacement payment. Use `postalform.create_machine_order` or the machine REST endpoints when the agent should fill the PDF/letter/form payload, create the order, and answer an x402 or MPP payment challenge itself.

If your agent uses workflow tools (`postalform.list_forms` + `postalform.get_form_schema`), treat `checkout_flow` in schema responses as optional informational metadata for PostalForm web routing. It does not change MCP call order.

## Refunds
Stripe supports refunds for crypto pay-ins in live mode. If you need to reverse a machine payment, you can refund the Stripe PaymentIntent and have USDC sent back to the payer wallet.

Contact support@postalform.com if you need help enabling machine payments or wiring up a facilitator for your deployment.
