Skip to contentPostalForm

Machine payments overview

Legacy x402

Legacy 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.
  4. Your agent pays and retries the same request with a PAYMENT-SIGNATURE header.
  5. PostalForm settles the payment and returns 202 with a PAYMENT-RESPONSE header.

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
  • x402 manifest: GET https://postalform.com/.well-known/x402
  • x402 manifest alias: GET https://postalform.com/.well-known/x402.json

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

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

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.

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

Bulk mailing overview

Bulk mailing uses the same validate, create, and poll endpoints as the single-recipient machine order flow. The difference is that instead of one recipient address, you send one sender address plus a bulk object that describes the CSV list and content mode.

Required bulk fields:

  • csv_content: the raw CSV text
  • 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: PostalForm merges {{column_name}} variables into trusted styled HTML and renders one PDF letter per row.
  • 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

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

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:

  • US addresses only.
  • 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 are US-only and must include:

  • line1 (street address)
  • line2 (optional unit/suite)
  • city
  • state (two-letter US state code, e.g. CA)
  • zip (ZIP or ZIP+4)

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:

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

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).
  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:

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

Example drill-down (Container -> Address):

{
  "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), you can use manual address input instead of Loqate IDs.

Draft an order payload (legacy 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)
  • pdf (recommended canonical format: { "upload_token": "..." }; also accepts { download_url, file_id }, a data:application/pdf;base64,... URL, or an allowlisted https URL)
  • 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)
  • mailpiece_type (letter or postcard; default letter)
  • postcard_size (4x6, 6x9, 11x6; required when mailpiece_type="postcard")

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.
  • Follow the published postcard PDF guidelines and template downloads:
  • PostalForm normalizes postcard mail options server-side to:
    • color=true
    • double_sided=true
    • mail_class="standard"
    • certified=false
    • signature_required=false

You can mix-and-match:

  • Manual sender + Loqate recipient
  • Loqate sender + manual recipient

Example request:

{
  "request_id": "8c1a1b58-2c8f-4f4f-9c46-2c1ac32d7a1b",
  "buyer_name": "Agent Owner",
  "buyer_email": "[email protected]",
  "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 (manual addresses):

{
  "request_id": "8c1a1b58-2c8f-4f4f-9c46-2c1ac32d7a1b",
  "buyer_name": "Agent Owner",
  "buyer_email": "[email protected]",
  "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):

{
  "request_id": "8c1a1b58-2c8f-4f4f-9c46-2c1ac32d7a1b",
  "buyer_name": "Agent Owner",
  "buyer_email": "[email protected]",
  "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 (bulk mailing with merge fields):

{
  "request_id": "8c1a1b58-2c8f-4f4f-9c46-2c1ac32d7a1b",
  "buyer_name": "Agent Owner",
  "buyer_email": "[email protected]",
  "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.

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: state must be a valid two-letter US code and zip must be ZIP or ZIP+4.
    • Loqate addresses: *_address_type must be "Address" (not "Container"), and ids must be US 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.
    • 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".
    • 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.
  • 409 (request_id_mismatch): the same request_id was reused with a different payload.
  • 402 (payment_required): pay and retry with PAYMENT-SIGNATURE (or use purl).
  • 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.

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: ["tempo", "stripe_spt"].
  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 typically returns both a tempo challenge and a stripe challenge for the same order.
  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.
  • 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.

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.
  • 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: Tempo with mppx

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

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":"[email protected]","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 ....

Pay autonomously (legacy 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.

Track fulfillment

Tracking depends on the payment path:

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

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

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.get_order_status (poll status for non-machine draft/checkout flows)

If your agent also 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 [email protected] if you need help enabling machine payments or wiring up a facilitator for your deployment.