Skip to contentPostalForm

MPP

PostalForm MPP endpoint guide

Use this file when an MPP registry reviewer or MPP-capable agent needs only PostalForm's MPP endpoints and payment instructions.

Published Jun 17, 2026 • Updated Jun 19, 2026

MPP endpoints

Document mail, workflow forms, postcards, and bulk mail:

Purpose Method and path
Discovery probe challenge GET /api/machine/mpp/orders
Validate and quote without payment side effects POST /api/machine/mpp/orders/validate
Create unpaid order, receive challenge, and retry after payment POST /api/machine/mpp/orders
Poll payment and fulfillment status GET /api/machine/mpp/orders/{id}

Flower letters:

Purpose Method and path
Validate product, ZIP, delivery date, and final quote POST /api/machine/mpp/flower-letters/validate
Create unpaid flower order, receive challenge, and retry after payment POST /api/machine/mpp/flower-letters
Poll payment and florist submission status GET /api/machine/mpp/flower-letters/{id}

Flower catalog prep endpoints are not MPP-paid, but agents need them before creating a flower-letter MPP order:

Purpose Method and path
List flower categories GET /api/flowers/categories
List flower products GET /api/flowers/products?category=bs&count=18&start=1&sorttype=pa
List delivery dates for a ZIP GET /api/flowers/delivery-dates?zipcode=32503

Payment flow

  1. Validate first. Use the relevant /validate endpoint and show the quote to the owner.
  2. Create the unpaid order with the same JSON body and no Authorization header.
  3. PostalForm returns HTTP 402 with one or more WWW-Authenticate: Payment ... headers.
  4. Parse the returned challenges and choose exactly one supported method.
  5. Pay the challenge with an MPP-capable client.
  6. Retry the exact same create request with the exact same JSON body and request_id, adding Authorization: Payment <serialized-mpp-credential>.
  7. Read the Payment-Receipt header on success.
  8. Poll the returned status endpoint until payment_status is paid.

Do not create a second order after the 402. Reuse the same request_id and byte-equivalent JSON body. If the body changes for an existing request_id, PostalForm returns 409 request_id_mismatch.

Payment methods

Use one of these methods for this MPP verification guide:

  • tempo: settle the Tempo challenge with an MPP-capable Tempo client such as mppx.
  • stripe_spt: mint or obtain a buyer-approved Stripe Shared Payment Token (spt_...) and serialize it into the Stripe MPP credential.

Document mail quick start

Use POST /api/machine/mpp/orders/validate, then POST /api/machine/mpp/orders.

Document-mail MPP addresses follow the same country configuration as PostalForm's checkout address picker. Manual countryCode values and Loqate ID country prefixes must be one of US, CA, AT, BE, CH, DE, FR, GB, IN, LU, or NL. Omitted manual-address countries default to US; unsupported or invalid codes are rejected. Bulk CSV rows use the same list through an optional country, country_code, or countrycode column.

curl -sS https://postalform.com/api/machine/mpp/orders/validate \
  -H 'Content-Type: application/json' \
  --json '{
    "request_id": "8c1a1b58-2c8f-4f4f-9c46-2c1ac32d7a1b",
    "buyer_name": "Agent Owner",
    "buyer_email": "owner@example.com",
    "letter": {
      "title": "MPP verification letter",
      "body": "Hello,\n\nThis is a PostalForm MPP verification letter.\n\nSincerely,\nAgent Owner",
      "format": "text",
      "signature": "Agent Owner"
    },
    "sender_name": "Agent Owner",
    "sender_address_type": "Manual",
    "sender_address_manual": {
      "line1": "123 Sender St",
      "city": "Springfield",
      "state": "IL",
      "zip": "62701",
      "countryCode": "US"
    },
    "recipient_name": "Recipient Example",
    "recipient_address_type": "Manual",
    "recipient_address_manual": {
      "line1": "456 Recipient Ave",
      "city": "Springfield",
      "state": "IL",
      "zip": "62701",
      "countryCode": "US"
    },
    "double_sided": true,
    "color": false,
    "mail_class": "standard",
    "certified": false
  }'

Create the unpaid order using the same body:

curl -i https://postalform.com/api/machine/mpp/orders \
  -H 'Content-Type: application/json' \
  --json @mpp-mail-body.json

On 402, choose one returned MPP challenge and retry:

curl -i https://postalform.com/api/machine/mpp/orders \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Payment <serialized-mpp-credential>' \
  --json @mpp-mail-body.json

Poll:

curl -sS https://postalform.com/api/machine/mpp/orders/8c1a1b58-2c8f-4f4f-9c46-2c1ac32d7a1b

See the mail-only version: https://postalform.com/mpp-mail.md

Flower-letter quick start

Use the flower catalog endpoints first, then validate and create with the MPP flower endpoints.

curl -sS https://postalform.com/api/machine/mpp/flower-letters/validate \
  -H 'Content-Type: application/json' \
  --json '{
    "request_id": "0bc151ff-31a9-47a6-a0a4-2853cd75ff2a",
    "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-03",
    "customer": {
      "name": "Agent Owner",
      "phone": "8505550100",
      "address1": "123 Sender St",
      "city": "Pensacola",
      "state": "FL",
      "zipcode": "32503",
      "country": "US"
    },
    "recipient": {
      "name": "Recipient Example",
      "phone": "8505550199",
      "address1": "456 Recipient Ave",
      "city": "Pensacola",
      "state": "FL",
      "zipcode": "32503",
      "country": "US"
    },
    "allow_substitutions": true
  }'

Create and retry after payment using the same POST /api/machine/mpp/flower-letters body. Poll GET /api/machine/mpp/flower-letters/{id}.

See the flower-only version: https://postalform.com/mpp-flowers.md

Stripe SPT credential shape

For Stripe SPT, the retry header must contain the serialized MPP credential, not the bare spt_... token.

import { Challenge, Credential } from 'mppx'

const unpaid = await fetch('https://postalform.com/api/machine/mpp/orders', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(body),
})

if (unpaid.status !== 402) throw new Error(`Expected 402, got ${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 spt = await mintSharedPaymentGrantedToken({
  amount: stripeChallenge.request.amount,
  currency: stripeChallenge.request.currency,
  networkId: stripeChallenge.request.methodDetails?.networkId,
  externalId: body.request_id,
})

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

const paid = await fetch('https://postalform.com/api/machine/mpp/orders', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': authorization,
  },
  body: JSON.stringify(body),
})

The token must match the challenge amount, currency, seller network id, and validity window.

Tempo client shape

If your verifier uses mppx, it can parse the WWW-Authenticate: Payment ... challenge, settle it, and retry:

mppx \
  https://postalform.com/api/machine/mpp/orders \
  --account my-tempo-wallet \
  --json-body "$(cat mpp-mail-body.json)" \
  --include \
  --verbose

Status values

Document mail status responses include:

  • awaiting_payment: unpaid draft exists and can be retried with Authorization: Payment ....
  • settled_pending_webhook: payment was accepted and PostalForm is waiting for final payment finalization.
  • paid: payment is finalized and fulfillment has been dispatched or queued.

Flower-letter status responses include:

  • awaiting_payment: unpaid flower order exists.
  • settled_pending_submission: payment was accepted and florist submission is queued.
  • paid: payment was accepted and submission processing has started.

Errors agents should handle

  • 400 or 422 invalid_request: payload validation failed. Read the errors array and fix the request before paying.
  • 402 payment_required: normal MPP challenge response. Pay one challenge and retry the same create request.
  • 409 request_id_mismatch: the same request_id was reused with a different payload. Use the original body or a fresh UUID.
  • 422 missing_buyer_email: buyer_email is required for machine payments and Stripe receipt delivery.
  • 429 rate_limited: slow down unpaid order creation and retry later.
  • 500 payment_error: payment verification failed or the payment provider returned an error.

Machine-readable references

  • MPP mail guide: https://postalform.com/mpp-mail.md
  • MPP flower guide: https://postalform.com/mpp-flowers.md
  • OpenAPI: https://postalform.com/openapi.json
  • Broader agent guide: https://postalform.com/agents.md
  • Agent skill: https://postalform.com/skill.md