Batch & Postcards
Send hundreds or thousands of postcards, letters, flyers and pamphlets from one PDF and one CSV. A 4×6 postcard is $1.05 and a one-sheet letter $1.15, printing and First-Class postage included (all prices). Price it, pay with prepaid credits, hand it to the facility, and hear back on your webhook. The same routes run in the sandbox with an sk_agent_test_ key: identical responses, no credits, no printing.
Nothing is charged until confirm. A draft holds the exact price; confirm debits it once from prepaid credits and is idempotent on the batch. A partial or failed fulfillment refunds the pieces that were not mailed.
The flow
- Estimate
POST /v1/batch-mail/estimateprices a mail type, class, quantity and page count. No upload. - Create a draft
POST /v1/batch-mailwith the template (a PDF, or a PNG/JPG postcard graphic) and the recipients, as file parts or as JSON withtemplate_urland inline recipients, returns a draft with the exact price and every row that will be skipped. Nothing is charged; a draft not confirmed within 7 days is cancelled and its files are removed. - Check the proof
GET /v1/batch-mail/{id}/proof?format=jsonreturnsproof_url, a link that opens in any browser for an hour: the template as it prints plus the address label for any recipient, for the person who signs off before money moves. Withoutformat=jsonthe route answers the PDF itself to a client that sends the API key. - Confirm
POST /v1/batch-mail/{id}/confirmdebits credits and queues the batch. Pieces are generated, the facility is alerted, and you receivebatch.submitted. - Track
GET /v1/batch-mail/{id}or your webhook:batch.completed,batch.partially_completed,batch.failed,batch.cancelled.
Bearer key with the mail.send scope. Agent-scoped keys also send X-Mailbox-MD-Version, as on POST /v1/mail. Send a unique Idempotency-Key on create and confirm. The key's policies hold here too: a force_approval key creates the draft but the account owner pays for it from the dashboard, max_daily_pieces counts the whole batch, and X-Max-Cost-Cents on confirm caps the debit.
Outbound postal mail
A batch is outbound postal mail, the same as a single POST /v1/mail letter: printed at the facility in Manhattan Beach, CA, addressed from your CSV, and handed to USPS with the return address on your account.
First-Class Mail is the default. mailing_class: first_class sends postcards, letters, flyers and pamphlets as USPS First-Class Mail: postcards from one piece, letters, flyers and pamphlets from 10 (for fewer, send each with POST /v1/mail), up to 10,000 a batch. USPS forwards First-Class Mail when the recipient has moved and returns it to your return address when it cannot be delivered, at no extra charge, so the returns from a First-Class batch tell you which addresses are dead. That is why every postcard carries your return address on its address side.
Marketing Mail is the volume class. mailing_class: marketing_mail needs 200 valid recipients, travels more slowly, and USPS neither forwards nor returns it, so an undeliverable piece is simply gone. Use it for announcements where the per-piece price matters more than speed or a clean list. Not open yet: until the facility's USPS Marketing Mail permit is active, the estimate and create routes answer 400 BATCH_MARKETING_MAIL_UNAVAILABLE for this class; send the batch as first_class.
The class names are USPS postage classes, not descriptions of the content: a realtor's 500 postcards can go either way. The content only limits the choice in one direction: bills, statements of account and anything with the character of personal correspondence must go First-Class (DMM 243). Both classes run through the same estimate, draft, confirm and webhook steps; the class is one field on the batch and changes only the price and the delivery window.
Pricing
Per piece: printing, card stock or paper, the envelope where one is used, the address label, handling and USPS postage. First-Class: 4×6 postcard $1.05, 6×9 postcard $1.35 (both sides in full color; a one-page design gets an address side printed with the return address), letter $1.15 (1 sheet) or $1.35 (2–3 sheets), flyer $1.15, pamphlet $1.95 (4–8 pages) or $2.75 (9–16 pages). Marketing Mail (200+ recipients, not open yet): postcards 85¢, letters and flyers 89¢, pamphlets $1.75. Color on letters, flyers and pamphlets adds 16¢ per color page; duplex is included (two pages per sheet). Volume: 5 % off at 500+, 10 % at 1,000+. No piece is priced below its minimum, our cost to make and mail it plus 5¢: two-sided color, extra sheets, the volume discount and account prices all stop there, and the estimate says when it applied. Postcards from 1 piece (a $2.00 small-batch fee under 10); letters, flyers and pamphlets from 10 (fewer: POST /v1/mail); up to 10,000 per batch. The estimate is the exact amount confirm debits.
| mail_type | Template pages | First-Class | Marketing Mail | Color |
|---|---|---|---|---|
| postcard_4x6 | 1 page (2 with duplex: front and back) | $1.05 | 85¢ | included, both sides |
| postcard_6x9 | 1 page (2 with duplex: front and back) | $1.35 | $1.03 | included, both sides |
| letter | 1–3 pages (up to 6 with duplex) | $1.15 (1 sheet) · $1.35 (2–3 sheets) | 89¢ | +16¢ per color page |
| flyer | 1 page (2 with duplex) | $1.15 | 89¢ | +16¢ per color page |
| pamphlet_light | 4–8 pages | $1.95 | $1.75 | +16¢ per color page |
| pamphlet_heavy | 9–16 pages | $2.75 | $1.75 | +16¢ per color page |
Example: 1,000 4×6 postcards First-Class = $1.05 × 1,000 = $1,050.00. The 10 % tier would take $105.00 off, but no card goes below its $1.01 minimum, so the discount stops at $40.00 and the total is $1,010.00. Five postcards: 5 × $1.05 plus the $2.00 small-batch fee = $7.25. Classes: first_class (1-5 day delivery); marketing_mail (3-10 day, min 200 pieces). A template that does not fit its mail type is refused with BATCH_TEMPLATE_PAGES_INVALID or BATCH_TEMPLATE_SIZE_INVALID before anything is uploaded.
Why a minimum: USPS single-piece postage is 65¢ for a postcard and 78¢ for a metered letter (82¢ stamped), plus 29¢ an ounce, and a 6×9 card pays the letter rate. Until the facility presorts, a volume discount comes out of printing and handling, so it stops at each piece's cost plus 5¢, and so do account prices. The estimate shows minimum_per_piece_cents and minimum_applied. There is no monthly platform fee.
Template PDF
One PDF prints for every recipient; the recipient address goes on a label the facility applies, so the template needs no merge fields and no address block. Export it at the finished size of the piece, with up to 0.125 in of bleed per edge; either orientation is accepted. A PDF at another size is refused with BATCH_TEMPLATE_SIZE_INVALID before anything is uploaded (a US Letter PDF is a letter or a flyer, not a 4×6 postcard).
| mail_type | Page size | Pages |
|---|---|---|
| postcard_4x6 | 4 × 6 in or 4.25 × 6 in | 1 page (2 with duplex: front and back) |
| postcard_6x9 | 6 × 9 in | 1 page (2 with duplex: front and back) |
| letter | US Letter (8.5 × 11 in) or A4 | 1–3 pages (up to 6 with duplex) |
| flyer | US Letter (8.5 × 11 in) or A4 | 1 page (2 with duplex) |
| pamphlet_light | US Letter (8.5 × 11 in) or A4 or half letter (5.5 × 8.5 in) | 4–8 pages |
| pamphlet_heavy | US Letter (8.5 × 11 in) or A4 or half letter (5.5 × 8.5 in) | 9–16 pages |
Postcards are printed on both sides. With a one-page template (a single image, say) we print the address side: your return address in the upper left, the right half clear for the address label and postage. With a two-page template (duplex), page 2 is the address side: put your return address in its upper left and keep its right half clear for the label and postage (USPS DMM 201) and its bottom 5/8 inch clear for the USPS barcode. Letters, flyers and pamphlets are folded into a #10 envelope (a 6×9 envelope above five sheets) with the label, which also shows your return address, and mailed at the USPS letter rate; nothing is mailed flat, so pamphlet pages stop at letter size. Duplex prints two pages per sheet and is included.
A postcard graphic instead of a PDF. A PNG or JPG is accepted as the template, as a file part or as template_url: it is set on one page at the mail type's size, in the image's orientation, scaled to fill and centred, so the graphic bleeds to the edge. Design at the finished size at 300 dpi (a 4×6 postcard is 1,200 × 1,800 px; a 6×9 is 1,800 × 2,700 px). The response says how the image was taken (template.image: pixel size, effective dpi, page) and warns in warnings when it is below 300 dpi or its proportions do not match the page and part of it is cropped. Anything other than a PDF, PNG or JPG is refused with BATCH_TEMPLATE_FORMAT_INVALID.
Recipients CSV
Columns name, address, city, state, zip; address2 (apt or suite) is optional. Header aliases are understood (full_name, street, st, zipcode, postal_code…). UTF-8 or Windows-1252, comma, semicolon or tab, quoted fields with commas or line breaks, up to 10,000 rows and 5 MB.
name,address,address2,city,state,zip
Jane Smith,123 Main St,,Los Angeles,CA,90210
Acme Inc,"456 Oak Ave, Suite 2B",,New York,New York,10001-4321States accept codes or names (states, DC, territories, military). ZIP and ZIP+4 are normalised; a four-digit ZIP is flagged as a dropped leading zero with the fix, and a ZIP whose prefix belongs to another state than the row names is flagged as a mismatch. Duplicates (same name at the same address) and malformed rows are skipped, listed in recipients.skipped_rows with the reason, and never charged. The count in recipients.valid is the count you pay for and the count the facility prints.
Your own headers. Headers are matched by common aliases (full_name, street, st, zipcode…). When a file uses headers we do not recognise, send column_map: a JSON object from field to the header text in your file. The dashboard shows the same mapping step whenever a required column is missing.
{ "name": "Customer", "line1": "Street 1", "line2": "Street 2", "city": "Town", "state": "ST", "zip": "Postal" }Recipients without a file. With a JSON body, recipients is an array of objects (name or company, line1, optional line2, city, state, zip; keys like address, address2, postal_code are understood), or recipients_url is a public link to the CSV. The same validator runs: skipped rows are reported by position in the array (1 = first) with the reason.
Endpoints
Rendered from the OpenAPI document; openapi.json is the contract. Errors are { error, code, retryable, suggested_action }.
POST /v1/batch-mail/estimate — Price a batch before uploading anything
Per piece: printing, card stock or paper, the envelope where one is used, the address label, handling and USPS postage. First-Class: 4×6 postcard $1.05, 6×9 postcard $1.35 (both sides in full color; a one-page design gets an address side printed with the return address), letter $1.15 (1 sheet) or $1.35 (2–3 sheets), flyer $1.15, pamphlet $1.95 (4–8 pages) or $2.75 (9–16 pages). Marketing Mail (200+ recipients, not open yet): postcards 85¢, letters and flyers 89¢, pamphlets $1.75. Color on letters, flyers and pamphlets adds 16¢ per color page; duplex is included (two pages per sheet). Volume: 5 % off at 500+, 10 % at 1,000+. No piece is priced below its minimum, our cost to make and mail it plus 5¢: two-sided color, extra sheets, the volume discount and account prices all stop there, and the estimate says when it applied. Postcards from 1 piece (a $2.00 small-batch fee under 10); letters, flyers and pamphlets from 10 (fewer: POST /v1/mail); up to 10,000 per batch. The estimate is the exact amount confirm debits. Sandbox keys get the same numbers with credits_required_cents 0. Refuses a template page count that does not fit the mail type (400 BATCH_TEMPLATE_PAGES_INVALID), fewer than 10 letters, flyers or pamphlets (400 BATCH_MINIMUM_PIECES; postcards start at one), Marketing Mail under 200 recipients (400 BATCH_MARKETING_MAIL_MINIMUM) and Marketing Mail before the facility's permit is active (400 BATCH_MARKETING_MAIL_UNAVAILABLE). stock and insert_stock add the field kit add-ons (field_kit_cents).
POST /api/v1/batch-mail/estimate
Authorization: Bearer sk_agent_test_...
Content-Type: application/json
{ "mail_type": "postcard_4x6", "mailing_class": "first_class", "quantity": 1000, "page_count": 1 }
200 { "estimate": { "per_piece_cents": 105, "color_surcharge_cents": 0, "effective_per_piece_cents": 105,
"minimum_per_piece_cents": 101, "minimum_applied": true, "volume_tier_pct": 10, "volume_discount_pct": 3.81,
"subtotal_cents": 105000, "discount_cents": 4000, "small_batch_fee_cents": 0, "total_cents": 101000,
"credits_required_cents": 0, "sandbox": true } }POST /v1/batch-mail — Create a batch draft (nothing charged yet)
Two request forms. multipart/form-data with `template` (PDF, PNG or JPG, ≤10 MB; a PDF has ≤200 pages and its page count and page size must fit the mail type: 400 BATCH_TEMPLATE_PAGES_INVALID or BATCH_TEMPLATE_SIZE_INVALID; a PNG or JPG is set on one page at the mail type's size, scaled to fill) and `recipients` (CSV, ≤5 MB, ≤10,000 rows, columns name, address, city, state, zip; address2/apt optional; UTF-8 or Windows-1252; comma, semicolon or tab). Or application/json with `template_url` (a public https:// link to the same file kinds) and either `recipients` (inline objects) or `recipients_url` (a CSV link), for agents in an IDE or a chat app that hold links rather than files. Headers are matched by common aliases; when yours differ, send `column_map`, from field (name, line1, line2, city, state, zip) to your header text, e.g. {"name":"Customer","line1":"Street 1","city":"Town","state":"ST","zip":"Postal"}. Malformed rows and duplicates are skipped and listed, never charged. A one-page postcard gets an address side printed with the return address (USPS returns undeliverable cards only to a return address on that side). The response carries the exact price confirm will debit; GET /v1/batch-mail/{id}/proof?format=json then gives the person who checks the batch a link to the template plus the address label for any recipient. Drafts expire after 7 days. Field kits, all optional: every recipient column or key that is not part of the address is a field, printed by {{tag}} in message or qr.url ({{tag|fallback}} when it may be empty) and into PDF form fields of the same name; message is a letter's whole text, or the text on a postcard's address side; qr prints a code unique to each piece (https://mailbox.bot/q/<code>) that opens your link (link), a one-button page (confirm) or an address check (verify: a QR code plus a short code the person enters in your app, signed in, checked with POST /v1/batch-mail/verify and their ref; a scan opens qr.url with the code after #; nothing on mailbox.bot verifies; codes expire after 30 days by default), every scan and confirmation is counted per piece (times only, never who scanned), and the first scan and first confirmation of each piece go out as batch.piece.scanned and batch.piece.confirmed with your ref (a ref column); stock weatherproof prints letters and flyers on tear- and water-resistant synthetic sheets (+85¢ a sheet); one insert sheet rides in every envelope piece (paper +30¢, weatherproof +95¢, a full-sheet weatherproof sticker +$1.45); mail_on holds the batch until a date up to 60 days ahead. Fields, QR codes and the mail date are included; the add-ons raise the per-piece minimum by their cost.
curl -X POST https://mailbox.bot/api/v1/batch-mail \
-H "Authorization: Bearer sk_agent_..." -H "X-Mailbox-MD-Version: 2" \
-H "Idempotency-Key: q2-recall-0001" \
-F template=@recall.pdf -F recipients=@customers.csv \
-F job_name="Q2 Recall Notices" -F mail_type=letter -F mailing_class=first_class -F duplex=true \
-F column_map='{"name":"Customer","line1":"Street 1","city":"Town","state":"ST","zip":"Postal"}'
201 { "batch": { "id": "…", "status": "draft", "test_mode": false,
"recipients": { "total_rows": 1203, "valid": 1198, "skipped": 5,
"skipped_rows": [ { "row": 17, "field": "zip", "value": "2134", "error": "\"2134\" is 4 digits; a leading zero was probably dropped…" } ] },
"pieces": { "total": 1198, "created": 0, "mailed": 0, "failed": 0 },
"pricing": { "per_piece_cents": 115, "volume_discount_pct": 3.48, "total_cents": 132978, "credits_charged": false, "refunded_cents": 0 } },
"next_step": "Show a person the proof first: GET /v1/batch-mail/…/proof?format=json returns proof_url, which opens in any browser for an hour. Then POST /v1/batch-mail/…/confirm charges 132978 cents in credits and queues the batch" }
# The same create as JSON, from an IDE or a chat app: the postcard graphic by link, the recipients inline.
curl -X POST https://mailbox.bot/api/v1/batch-mail \
-H "Authorization: Bearer sk_agent_test_..." -H "X-Mailbox-MD-Version: 2" \
-H "Idempotency-Key: open-house-0001" -H "Content-Type: application/json" \
-d '{ "job_name": "Spring open house", "mail_type": "postcard_4x6",
"template_url": "https://cdn.example.com/open-house-front.png",
"recipients": [ { "name": "Jane Smith", "line1": "123 Main St", "city": "Los Angeles", "state": "CA", "zip": "90210" },
{ "name": "Acme Inc", "line1": "456 Oak Ave", "line2": "Suite 2B", "city": "New York", "state": "NY", "zip": "10001" } ] }'
201 { "batch": { "id": "…", "status": "draft", "test_mode": true,
"links": { "proof": "https://mailbox.bot/api/v1/batch-mail/…/proof", "pieces": "…/pieces", "dashboard": "https://mailbox.bot/dashboard/mail?filter=batch" } },
"template": { "source": "image", "image": { "format": "png", "width": 1200, "height": 1800, "dpi": 300, "page": "4 × 6 in" } },
"next_step": "POST /v1/batch-mail/…/confirm — sandbox: no credits are charged. The proof for a reviewer is GET /v1/batch-mail/…/proof." }GET /v1/batch-mail — List batches
Newest first. Sandbox keys list sandbox batches only; agent keys see their own agent's batches.
GET /api/v1/batch-mail?status=submitted&limit=25&offset=0
Authorization: Bearer sk_agent_...
200 { "batches": [ … ], "pagination": { "total": 3, "limit": 25, "offset": 0, "has_more": false } }GET /v1/batch-mail/{id} — Get a batch with its first 50 pieces
{ "batch": { "id": "…", "status": "partially_completed",
"pieces": { "total": 1198, "created": 1198, "mailed": 1190, "failed": 8 },
"pricing": { "total_cents": 132978, "credits_charged": true, "refunded_cents": 888 },
"facility_notes": "8 not mailed: damaged in printing" },
"pieces": [ { "batch_sequence": 1, "recipient_name": "Jane Smith", "recipient_city": "Los Angeles", "recipient_state": "CA", "recipient_zip": "90210", "status": "mailed", "mailed_at": "2026-09-26T18:02:11Z" } ] }DELETE /v1/batch-mail/{id} — Cancel before the facility starts (full credit refund)
Allowed from draft, paid, generating and submitted. Once the facility has started (in_progress) the batch cannot be cancelled. Idempotent.
{ "id": "…", "status": "cancelled", "refunded_credits_cents": 132978, "sandbox": false }GET /v1/batch-mail/{id}/pieces — List every piece of a batch, paginated
One row per recipient in mailing order with the piece's status (submitted, ready, mailed, failed, cancelled), mailed_at and error_message, and qr (its code, printed link, scans, first_scanned_at, confirmed_at) on a batch with QR codes. Filter by status to list what was not mailed after a partial completion, or by qr (scanned, confirmed, not_scanned) to see who responded. GET /v1/batch-mail/{id} carries the first 50; read the rest here.
GET /api/v1/batch-mail/{id}/pieces?status=failed&limit=100&offset=0
Authorization: Bearer sk_agent_...
200 { "batch_id": "…",
"pieces": [ { "id": "…", "batch_sequence": 212, "recipient_name": "Pat Lee", "recipient_line1": "9 Elm St", "recipient_line2": null,
"recipient_city": "Dayton", "recipient_state": "OH", "recipient_zip": "45402", "status": "failed", "mailed_at": null,
"error_message": "Not mailed: batch partially completed by the facility" } ],
"pagination": { "total": 8, "limit": 100, "offset": 0, "has_more": false } }GET /v1/batch-mail/{id}/proof — A proof PDF: the template, then the address label for one recipient
For the person checking an agent's batch before it is paid: the stored template page for page (on a field kit batch, recipient N's own piece: fields filled in, the QR code where it prints, a sample code until the pieces exist, then the insert), followed by one page with the address label the facility applies for recipient N (default the first valid row), at the label's real size, next to the return address. Works for drafts, paid and sandbox batches. Answers application/pdf to a client that sends the API key (a script, curl, an IDE HTTP client); with format=json, or Accept: application/json, answers proof_url, a one-hour signed link to the same PDF that opens in any browser with no key, plus the label's lines. Give the person proof_url; it is what the MCP tool get_batch_mail_proof returns. 400 BATCH_PROOF_RECIPIENT_INVALID when recipient is past the last valid row.
GET /api/v1/batch-mail/{id}/proof?recipient=1
Authorization: Bearer sk_agent_...
200 Content-Type: application/pdf ← the template, then one page with the address label for recipient 1
GET /api/v1/batch-mail/{id}/proof?recipient=1&format=json
200 { "batch_id": "…", "recipient": 1, "of": 1198, "label": [ "JANE SMITH", "123 MAIN ST", "LOS ANGELES, CA 90210" ],
"proof_url": "https://…/proof-1.pdf?token=…", "expires_in": 3600, "page_count": 2 }POST /v1/batch-mail/{id}/confirm — Charge credits and queue the batch
Debits batch.pricing.total_cents from prepaid credits (Auto-Fill applies if enabled), generates one piece per valid recipient and hands the batch to the facility. Idempotent on the batch: repeating returns the current state without a second debit. 402 carries billing_url and the shortfall. The key's policies apply as on POST /v1/mail: a force_approval key cannot pay (403 BATCH_APPROVAL_REQUIRED; the account owner confirms from the dashboard), max_daily_pieces counts this batch against the agent's last 24 h (422 BATCH_DAILY_PIECE_LIMIT), and X-Max-Cost-Cents caps the debit (422 BATCH_COST_CAP_EXCEEDED). Sandbox keys: no charge; then poll until submitted and use /advance.
POST /api/v1/batch-mail/{id}/confirm
Authorization: Bearer sk_agent_...
Idempotency-Key: q2-recall-confirm-0001
200 { "batch": { "status": "paid", "pricing": { "total_cents": 132978, "credits_charged": true } },
"credits_charged_cents": 132978, "credit_balance_cents": 67022, "sandbox": false,
"next_step": "Pieces are being generated; the facility prints once status is submitted. Listen for batch.submitted / batch.completed on your webhook." }
402 { "error": "Insufficient credits. This batch requires $1,329.78 and your balance is $12.00.", "code": "INSUFFICIENT_CREDITS",
"billing_url": "https://mailbox.bot/dashboard/billing#credits", "credits_required_cents": 132978, "credit_balance_cents": 1200, "credit_shortfall_cents": 131778 }POST /v1/batch-mail/verify — Check an address verification code your user typed
For batches with qr.mode verify: each piece prints a QR code and a short code. When your user types the short code into your app, send it here with the key of the environment the batch was sent in. A match on one of your own pieces (mailed to ref, when you send the signed-in person's ref) records the verification and sends batch.piece.confirmed; the answer carries the piece and your ref, so your app marks that user verified. This and your dashboard are the only places an address is verified: a scan opens qr.url with the code after #, and mailbox.bot verifies nothing on its own pages. Repeating it answers already_verified true. 404 BATCH_VERIFY_CODE_NOT_FOUND when no piece of yours carries the code; 409 BATCH_VERIFY_REF_MISMATCH when it was mailed to another ref (nothing recorded, the code stays unused); 410 BATCH_VERIFY_CODE_EXPIRED after expires_days. Limited to 120 checks a minute per account.
POST /api/v1/batch-mail/verify
Authorization: Bearer sk_agent_...
Content-Type: application/json
{ "code": "k7d2 m9xq", "ref": "seller-48213" }
200 OK
{ "verified": true, "already_verified": false, "batch_id": "b5f0…",
"piece": 214, "ref": "seller-48213", "verified_at": "2026-11-06T15:02:40Z", "sandbox": false }POST /v1/batch-mail/{id}/advance — Sandbox: simulate the facility one step
sk_agent_test_ keys only. submitted → in_progress → completed; completion marks every piece mailed and fires batch.completed to your webhook. With {"qr_event":"scan"|"confirm","piece":N} it records a scan or a confirmation on piece N of a sandbox batch with QR codes instead, so an agent can test reading tracking without a phone. Live batches are advanced by the facility, never by the API.
{ "batch": { "status": "in_progress", "test_mode": true, "pieces": { "total": 2, "created": 2, "mailed": 0, "failed": 0 } },
"previous_status": "submitted", "sandbox": true }From an IDE, Cursor or a chat assistant
An agent working in Cursor, Claude, ChatGPT or any MCP client has links, not files. The JSON form of create takes the template by template_url (a PDF, or a PNG/JPG postcard graphic) and the recipients inline, so a batch can be set up from a chat or an editor without a multipart upload. The same functions run behind the MCP tools, with the same prices, validation and key policies, and a sandbox key runs all of it free.
- Price
estimate_batch_mail(orPOST /v1/batch-mail/estimate): mail type, class, quantity, pages. - Draft
create_batch_mailwithtemplate_urlandrecipients(orrecipients_url). Nothing is charged; the result carries the exact price, the first recipients, any image warnings andlinks.proof. - The person checks
get_batch_mail_proofreturnsproof_url, a link that opens in any browser or IDE for an hour with no key: the template as it prints, then the address label for a chosen recipient at its real size, next to the return address (recipientpicks another row).links.proofon the batch is the API route itself and needs the API key. This is the step where a human says yes. - Pay
confirm_batch_maildebits prepaid credits once;max_cost_centsrefuses before any debit. A key withforce_approvalcannot pay: the draft waits for the account owner in the dashboard, where the same proof is one click away. - Follow
get_batch_mail,list_batch_mail,cancel_batch_mail; in the sandbox,advance_test_batch_mailwalks the batch tocompletedand firesbatch.completed.
Links are fetched by mailbox.bot from public HTTPS hosts only (signed cloud-storage URLs, a CDN, a GitHub raw URL); links that need a login, private hosts and IP addresses are refused with BATCH_SOURCE_URL_INVALID or BATCH_SOURCE_URL_UNREACHABLE. Templates are capped at 10 MB and recipient files at 5 MB. The tools are listed with the rest of the catalog on the MCP setup page; apps connected by sign-in get them with the mail.send permission.
Sandbox
An sk_agent_test_ key runs every route above against a sandbox batch: priced exactly like a live one, never charged (credits_required_cents is 0 and credits_charged stays false), invisible to the facility, and never in your live mail history. After confirm, poll until the batch is submitted, then call POST /v1/batch-mail/{id}/advance twice: submitted → in_progress → completed. Completion marks every piece mailed and fires batch.completed to your webhook, exactly as the facility would. Live batches are advanced by the facility, never by the API.
Webhooks
Subscribe to batch events with POST /v1/webhooks/endpoints or the webhook MCP tools. They are signed, retried and logged like every webhook, each sent at most once per batch, and a Sandbox webhook receives sandbox batches. An agent's callback URL still receives them in the older format, and the account owner gets an email at each step (see the webhooks overview).
| Event | Meaning |
|---|---|
| batch.submitted | Paid, every piece generated, in the facility's print queue. |
| batch.completed | The facility mailed every piece. |
| batch.partially_completed | The facility mailed some pieces; credits for the pieces it did not mail are refunded (pricing.refunded_cents). Mail USPS returns after it was mailed is not refunded. |
| batch.failed | The facility could not fulfill the batch; credits are refunded in full. |
| batch.cancelled | Cancelled before printing; credits are refunded. |
| batch.piece.scanned | A piece's QR code was scanned for the first time. Carries piece: sequence, your ref, mode, scans, times. |
| batch.piece.confirmed | The recipient confirmed the piece, or verified the address with its code (scan or typed). Once per piece. |
{
"schema_version": 2,
"event_id": "00000000-0000-4000-8000-000000000001",
"event_type": "batch.partially_completed",
"created_at": "2026-09-15T16:31:04Z",
"environment": "live",
"sample": false,
"batch": {
"id": "00000000-0000-4000-8000-000000000040",
"agent_id": "00000000-0000-4000-8000-000000000020",
"job_name": "Spring open house",
"status": "partially_completed",
"mail_type": "postcard_4x6",
"mailing_class": "first_class",
"pieces": {
"total": 1198,
"mailed": 1190,
"failed": 8
},
"total_cents": 120998,
"refunded_cents": 808,
"created_at": "2026-09-15T16:20:19Z",
"completed_at": "2026-09-15T16:31:04Z",
"error_message": "8 not mailed: damaged in printing"
},
"api_url": "https://mailbox.bot/api/v1/batch-mail/00000000-0000-4000-8000-000000000040"
}Errors
| code | Meaning |
|---|---|
| BATCH_TEMPLATE_PAGES_INVALID | 400 — the PDF's page count does not fit the mail type (see Pricing). Pick another type, turn on duplex, or change the PDF. |
| BATCH_TEMPLATE_SIZE_INVALID | 400 — a page of the PDF is not the size the mail type prints at (see Template PDF). Export at that size, or pick the matching mail type. |
| BATCH_TEMPLATE_FORMAT_INVALID | 400 — the template is not a PDF, PNG or JPG. |
| BATCH_SOURCE_URL_INVALID | 400 — template_url or recipients_url is not a public https:// link (private host, IP address, credentials, or a redirect to one). |
| BATCH_SOURCE_URL_UNREACHABLE | 400 — the link answered an error, timed out, needs a login or was empty. Use a signed or public URL and try again. |
| BATCH_SOURCE_TOO_LARGE | 400 — the linked file is over the limit (template 10 MB, recipients 5 MB). |
| BATCH_RECIPIENTS_INVALID | 400 — inline recipients exceed 10,000 entries. |
| BATCH_PROOF_RECIPIENT_INVALID | 400 — recipient on the proof route is past the last valid row. |
| BATCH_PROOF_UNAVAILABLE | 409 or 503 — the batch has no files any more (expired or cancelled draft), or storage is temporarily unavailable. |
| BATCH_MINIMUM_PIECES | 400 — letters, flyers and pamphlets start at 10 valid recipients (postcards at 1). Add recipients, or send each piece with POST /v1/mail. |
| BATCH_MARKETING_MAIL_MINIMUM | 400 — Marketing Mail needs at least 200 valid recipients. Use first_class or add recipients. |
| BATCH_MARKETING_MAIL_UNAVAILABLE | 400 — Marketing Mail is not open yet (the facility's USPS permit). Use first_class. |
| BATCH_APPROVAL_REQUIRED | 403 — the key has force_approval: the draft is saved and the account owner pays for it from the dashboard. Poll the batch. |
| BATCH_DAILY_PIECE_LIMIT | 422 — the key's max_daily_pieces: the agent's last 24 h plus this batch exceed the limit. Nothing charged. |
| BATCH_COST_CAP_EXCEEDED | 422 — the batch costs more than X-Max-Cost-Cents. Nothing charged. |
| INSUFFICIENT_CREDITS | 402 — carries billing_url and the shortfall. Add credits, then confirm again with a new Idempotency-Key. |
| BATCH_GENERATION_DISPATCH_PENDING | 503 — the batch is funded; generation is being retried automatically. Poll the batch; it moves to submitted on its own. |
| BATCH_MAIL_NOT_CANCELLABLE | 409 — the facility has started (in_progress) or the batch is already terminal. |
| BATCH_ADVANCE_INVALID | 409 — sandbox only: still generating, or already completed. |
| BATCH_MERGE_FIELD_UNKNOWN | 400 — a {{tag}} names no column in the recipients and no built-in field; the message lists what is available. |
| BATCH_MESSAGE_INVALID | 400 — the message does not fit the postcard's address side once filled in, the postcard template has its own back, or a letter sent both a template and a message. |
| BATCH_QR_INVALID | 400 — qr.mode link without qr.url, a link that is not https://, a label over 40 characters, or qr.on insert without an insert. |
| BATCH_STOCK_UNAVAILABLE | 400 — weatherproof stock on a postcard or pamphlet; it prints letters and flyers. |
| BATCH_INSERT_INVALID | 400 — an insert on a postcard, not US Letter, more than one sheet, or insert_stock without an insert file. |
| BATCH_MAIL_ON_INVALID | 400 — mail_on is not a date from today to 60 days ahead (Pacific time). |
| BATCH_QR_EVENT_INVALID | 409 — sandbox only: the batch has no QR codes, or its codes are not made yet (they are made when the pieces are generated). |
| MAILBOX_MD_VERSION_MISMATCH | 409 — agent keys send the current X-Mailbox-MD-Version, as on POST /v1/mail. |
Field kits: fields, a QR code on every piece, weatherproof stock, inserts
Every piece can differ. Each column of the recipients file that is not part of the address (or each extra key of an inline recipient, or a nested fields object) is a field. {{permit}} prints that recipient's value in message or qr.url, and a PDF form field named permit in the template or the insert fills with it. {{permit|to be confirmed}} prints the fallback when the value is empty; a row missing a value with no fallback is skipped and listed, never printed blank.
Every piece can answer. qr prints a code unique to each piece. link opens your page; confirm opens a mailbox.bot page with one button ("Sign posted", "Received"), then your page if you give one. Scans and confirmations are counted per piece.
| Option | What it does | Price |
|---|---|---|
| fields | Extra recipient columns or keys, printed by {{tag}} and into PDF form fields of the same name. Built-ins: name, address, address2, city, state, zip, sequence. | Included |
message | A letter's whole text (Times 12 pt, 1 in margins; send no template), or the text on a postcard's address side (send the front only; it must fit once filled in). | Included |
qr | mode link or confirm, url (may hold tags), label (up to 40 characters, printed beside the code), on piece or insert. | Included |
stock | weatherproof: letters and flyers on tear- and water-resistant synthetic sheets, for notices and small signs posted outdoors. | +85¢ a sheet |
insert_url + insert_stock | One US Letter sheet in every envelope piece, 1 or 2 pages: paper, weatherproof, or label_sheet (one full-sheet weatherproof sticker, one side). Not on postcards. | +30¢ · +95¢ · +$1.45 |
mail_on | The facility holds the pieces and mails them on or after this date (YYYY-MM-DD, Pacific time, up to 60 days ahead). | Included |
Add-ons raise the per-piece minimum by their cost (stock, printing, weight), so the estimate stays the exact amount confirm debits. Pass stock, insert_stock and insert_pages to the estimate to see field_kit_cents before any file exists.
POST /api/v1/batch-mail
Authorization: Bearer sk_agent_test_...
Content-Type: application/json
X-Mailbox-MD-Version: 2
Idempotency-Key: cup-2291-notices-0001
{
"job_name": "CUP-2291 site notices",
"mail_type": "letter",
"message": "Dear {{name}},\n\nThe hearing for permit {{permit}} is on {{hearing_date}}. Please post the enclosed sign at the site entrance.",
"recipients": [
{ "name": "Ana Ruiz", "line1": "18 Mesa Rd", "city": "Lancaster", "state": "CA", "zip": "93534", "permit": "CUP-2291", "hearing_date": "Nov 14" }
],
"qr": { "mode": "confirm", "label": "Sign posted", "on": "insert" },
"stock": "weatherproof",
"insert_url": "https://example.com/files/site-sign.pdf",
"insert_stock": "label_sheet",
"mail_on": "2026-11-03"
}How the codes are made
- When the pieces are generated (after confirm), each gets 12 random characters from 31 letters and digits (no 0, 1, i, l or o), about 59 bits, and the printed link
https://mailbox.bot/q/<code>. Its ownqr.urlis resolved from its fields then. - The code is drawn into the PDF as vector squares (error correction M, a four-module quiet zone): 0.8 in on a postcard's address side, bottom left, clear of the USPS barcode area; 0.7 in at the bottom right of page 1 on letters, flyers, pamphlets and inserts, with the label beside it. Keep about 3 × 1 in clear there; the proof shows exactly where it lands.
- Before the pieces exist, the proof prints a sample code that opens a page saying it is a proof.
- Verify pieces also get a short code to type: 8 characters from the same alphabet, printed in capitals as two groups, unique across all pieces. A code is checked only by your own key, against your own pieces, at 120 checks a minute per account, and with your user's
refa guessed code that belongs to someone else answers 409 and stays unused, so guessing gets nowhere. A live piece's short code is never returned by the API. - A scan stores a count and the first and last times; a confirmation stores its time. No IP address and no device details are kept, link previews and crawlers are not counted, and scans are rate limited per network.
Address verification
qr.mode verify turns a batch into proof that each recipient gets mail at the address: a business signing up on your marketplace, a vendor your agent is onboarding, a customer changing their address. Each piece prints its QR code and a short code (K7D2-M9XQ). The person enters the code where they are signed in: your app, which sends it to POST /v1/batch-mail/verify with that person's ref, so a code only counts for the person it was mailed to. Set qr.url to that page and a scan opens it with the code after the # (out of server logs), ready to submit once they sign in. No app? They read the code back to you and you enter it in the dashboard's Scans & Verification. mailbox.bot verifies nothing on its own pages. The first entry verifies the piece; the code stops working after qr.expires_days (30 by default, counted from the mail date). Put your own user id in a ref column and it comes back on the piece and on the webhook. No logins and no temporary accounts on our side: holding the mail, entered in the account that asked, is the proof. A postcard shows its code to anyone who handles it, which the ref check makes harmless; send a letter when you'd rather nobody saw it.
Tracking and webhooks
The batch carries tracking (pieces with codes, scanned, confirmed, last scan) and field_kit. Each piece on GET /v1/batch-mail/{id}/pieces carries qr (code, link, your ref, scans, first scan, confirmation, expiry), and ?qr=scanned, confirmed or not_scanned lists who has and has not answered. A piece's first scan sends batch.piece.scanned and its first confirmation (or verification) sends batch.piece.confirmed, each with the piece and your ref, to your webhooks and your agent's callback. In the sandbox, POST /v1/batch-mail/{id}/advance with {"qr_event":"scan","piece":1} (or confirm) records one as a phone would and fires the webhook, and sandbox verify pieces show their short_code so you can rehearse the verify call.
At the facility, a field kit batch prints from per-piece files in runs of 100, one file per stock, every page numbered, so piece N, its insert and label N are matched by number. A batch with mail_on is not marked mailed before that day.