MPP mail
PostalForm MPP mail endpoint instructions
Use this file for MPP-paid document mail only: letters, uploaded PDFs, workflow forms, postcards, and bulk mail campaigns.
Published Jun 17, 2026 • Updated Jun 19, 2026
Endpoints
| Purpose | Method and path |
|---|---|
| Discovery probe challenge | GET /api/machine/mpp/orders |
| Validate and quote without creating a payable draft | POST /api/machine/mpp/orders/validate |
Create unpaid draft, receive 402, and retry after MPP payment |
POST /api/machine/mpp/orders |
| Poll payment and fulfillment status | GET /api/machine/mpp/orders/{id} |
Flow
- Build one JSON body for the mail order.
- Send it to
POST /api/machine/mpp/orders/validate. - If valid, send the same body to
POST /api/machine/mpp/orderswithoutAuthorization. - Expect
402plusWWW-Authenticate: Payment ...headers. - Pay one returned challenge.
- Retry the exact same
POST /api/machine/mpp/ordersbody withAuthorization: Payment <serialized-mpp-credential>. - Read the
Payment-Receiptresponse header. - Poll
GET /api/machine/mpp/orders/{id}untilpayment_statusispaid.
Use the same request_id for validate, create, and paid retry. Generate a fresh UUID only for a different mailing.
Supported mail order types
Single recipient letter:
- Provide
letteras a string or object. - Optional
format:text,html,markdown, orrtf. - Optional
letter.signature: typed text or drawn signature data.
Uploaded PDF:
- Provide exactly one
pdfsource. - Preferred shape:
{ "pdf": { "upload_token": "upload_tok_..." } }. - Also accepted: a data URL string or
{ "download_url": "https://...", "file_id": "..." }.
Workflow form:
- Discover forms with
GET /api/machine/forms. - Fetch schema with
GET /api/machine/forms/{slug}/schema. - Send
form: { "slug": "...", "fields": { ... }, "attachments": [...] }. - Workflow forms are letter-only.
Postcard:
- Set
mailpiece_typetopostcard. - Set
postcard_sizeto4x6,6x9, or11x6. - Provide
pdfonly;letterandformare not allowed for postcards. - The PDF must follow the published two-page postcard layout guide returned as
postcard_guidelines_url.
Bulk mail:
- Include a top-level
bulkobject. - Include one sender address and no single-recipient fields.
- Use
bulk.content_modeoftext,html, orpdf. bulk.csv_contentis required.bulk.template_textis required for text mode.bulk.template_htmlis required for HTML mode.- Top-level
pdfis required for shared-PDF mode.
Required common fields
Every MPP mail body needs:
request_id: UUID idempotency key.buyer_name: buyer or agent owner name.buyer_email: required for machine payment receipt delivery.sender_name.- Sender address fields.
- Mailing options as needed:
double_sided,color,mail_class,certified,certified_return_receipt, andsignature_required.
For a single-recipient order, also include:
recipient_name.- Recipient address fields.
- Exactly one of
pdf,letter, orform.
For address fields, use one of these patterns:
- Loqate address:
sender_address_type: "Address",sender_address_id, andsender_address_text. - Manual address:
sender_address_type: "Manual"andsender_address_manual.
Manual address objects use line1, optional line2, city, state, zip, and optional countryCode.
MPP uses the same country choices as PostalForm's checkout address picker. countryCode and the country prefix on Loqate IDs 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 country codes are rejected instead of being treated as U.S. addresses.
Bulk recipient CSVs may use country, country_code, or countrycode. The same country list applies to every row, and omitted countries default to US.
Validate example
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": "markdown",
"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
}'
Expected validation shape:
{
"protocol": "mpp",
"methods": ["tempo", "stripe_spt"],
"status": "validated_new_order",
"quote": {
"price_usd": 3.4,
"currency": "usd",
"mailpiece_type": "letter"
}
}
For this guide, use tempo or stripe_spt.
Create and pay
curl -i https://postalform.com/api/machine/mpp/orders \
-H 'Content-Type: application/json' \
--json @mpp-mail-body.json
Expected unpaid response:
- HTTP status
402. X-Payment-Protocol: mpp.- One or more
WWW-Authenticate: Payment ...headers. - JSON body with
status: "payment_required". - For single-recipient drafts, a
preview_urlmay be present for reviewing the generated PDF before paying. - For postcards,
postcard_guidelines_urlmay be present.
Retry after paying one challenge:
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
Expected paid response:
- HTTP status
200when payment is finalized immediately, or202when settlement is accepted and finalization is pending. Payment-Receiptheader.- JSON body with
order_id,payment_status,payment_channel: "mpp_machine", andmachine_payment.
Stripe SPT requirements
For the Stripe challenge:
- Parse the Stripe challenge from
WWW-Authenticate. - Read the challenge amount, currency, and seller network id.
- Mint or obtain a buyer-approved
spt_...token for exactly those values. - Serialize the original challenge and
{ "spt": "..." }into one MPP credential. - Send the full serialized credential as
Authorization: Payment ....
Do not send only the bare spt_... token.
import { Challenge, Credential } from 'mppx'
const challenges = Challenge.fromResponseList(unpaidResponse)
const stripeChallenge = challenges.find((challenge) => challenge.method === 'stripe')
if (!stripeChallenge) throw new Error('No stripe challenge returned')
const authorization = Credential.serialize(
Credential.from({
challenge: stripeChallenge,
payload: { spt },
})
)
Tempo requirements
Tempo clients should parse the returned WWW-Authenticate: Payment ... challenge and retry with Authorization: Payment ....
mppx \
https://postalform.com/api/machine/mpp/orders \
--account my-tempo-wallet \
--json-body "$(cat mpp-mail-body.json)" \
--include \
--verbose
Poll status
curl -sS https://postalform.com/api/machine/mpp/orders/8c1a1b58-2c8f-4f4f-9c46-2c1ac32d7a1b
Status responses include:
awaiting_payment: unpaid draft exists.settled_pending_webhook: payment was accepted, but final payment finalization is pending.paid: payment is finalized and fulfillment has been dispatched or queued.
The {id} path can be the canonical order_id or a request id alias returned by validate/create.
Mail-specific errors
422 invalid_request: fix the payload before creating a payable order.409 request_id_mismatch: reuse the original JSON body or generate a fresh UUID for a new mailing.422 missing_buyer_email: add a validbuyer_email.402 payment_required: normal MPP challenge response.429 rate_limited: wait before creating more unpaid drafts.
Related MPP docs
- All MPP endpoints:
https://postalform.com/mpp.md - Flower-only MPP guide:
https://postalform.com/mpp-flowers.md - OpenAPI:
https://postalform.com/openapi.json