Skip to content
Back to PostalForm

Machine payments overview (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.
  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).

Address strategy (manual vs autocomplete)

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

Required fields:

  • request_id (UUID, used for idempotency)
  • 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)

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
}

Troubleshooting (common 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".
  • 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 (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

After a successful payment, poll:

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

The response includes is_paid, current_step, and x402 settlement metadata.

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>

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.