Browse documentation
DocumentationBatch & Postcards

Batch & Postcards

Send hundreds or thousands of postcards, letters, flyers and pamphlets from one PDF and one CSV. 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

  1. EstimatePOST /v1/batch-mail/estimate prices a mail type, class, quantity and page count. No upload.
  2. Create a draftPOST /v1/batch-mail with the template PDF and the recipients CSV returns a draft with the exact price and every row that will be skipped. Nothing is charged; drafts expire after 7 days.
  3. ConfirmPOST /v1/batch-mail/{id}/confirm debits credits and queues the batch. Pieces are generated, the facility is alerted, and you receive batch.submitted.
  4. TrackGET /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.

Pricing

Per piece, First-Class: postcards 28¢, letter 39¢ (1 sheet) or 54¢ (2–3 sheets), flyer 39¢, pamphlet 68¢ (4–8 pages) or 83¢ (9–16 pages). Marketing Mail (200+ recipients): postcards 28¢, letters 35¢, flyers 55¢, pamphlets 68¢. Colour adds 15¢ per piece; duplex is included (two pages per sheet). Volume: 5 % off at 500+, 10 % at 1,000+, 15 % at 5,000+. Up to 10,000 pieces per batch. Your account's price overrides apply; the estimate is the exact amount confirm debits.

mail_typeTemplateFirst-ClassMarketing Mail
postcard_4x61 page (2 with duplex: front and back)28¢28¢
postcard_6x91 page (2 with duplex: front and back)28¢28¢
letter1–3 pages (up to 6 with duplex)39¢ (1 sheet) · 54¢ (2–3 sheets)35¢
flyer1 page (2 with duplex)39¢55¢
pamphlet_light4–8 pages68¢68¢
pamphlet_heavy9–16 pages83¢68¢

Example: 1,000 colour 4×6 postcards First-Class = $0.43 × 1,000 = $430.00, less 10 % volume = $387.00. Classes: first_class (1-3 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 before anything is uploaded.

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.

CSV · recipients.csv
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-4321

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

JSON · column_map
{ "name": "Customer", "line1": "Street 1", "line2": "Street 2", "city": "Town", "state": "ST", "zip": "Postal" }

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, First-Class: postcards 28¢, letter 39¢ (1 sheet) or 54¢ (2–3 sheets), flyer 39¢, pamphlet 68¢ (4–8 pages) or 83¢ (9–16 pages). Marketing Mail (200+ recipients): postcards 28¢, letters 35¢, flyers 55¢, pamphlets 68¢. Colour adds 15¢ per piece; duplex is included (two pages per sheet). Volume: 5 % off at 500+, 10 % at 1,000+, 15 % at 5,000+. Up to 10,000 pieces per batch. Your account's price overrides apply; 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) and Marketing Mail under 200 recipients (400 BATCH_MARKETING_MAIL_MINIMUM).

HTTP · request and response
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, "color": true }

200 { "estimate": { "per_piece_cents": 28, "color_surcharge_cents": 15, "effective_per_piece_cents": 43,
       "volume_discount_pct": 10, "subtotal_cents": 43000, "discount_cents": 4300, "total_cents": 38700,
       "credits_required_cents": 0, "sandbox": true } }
POST /v1/batch-mail — Create a batch draft (nothing charged yet)

multipart/form-data with `template` (PDF, ≤10 MB, ≤200 pages, must fit the mail type) 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). Headers are matched by common aliases; when yours differ, send `column_map`, a JSON object 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. The response carries the exact price confirm will debit. Drafts expire after 7 days.

Shell · curl
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": 39, "volume_discount_pct": 10, "total_cents": 42050, "credits_charged": false, "refunded_cents": 0 } },
    "next_step": "POST /v1/batch-mail/…/confirm charges 42050 cents in credits and queues the batch" }
GET /v1/batch-mail — List batches

Newest first. Sandbox keys list sandbox batches only; agent keys see their own agent's batches.

HTTP · request
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
JSON · response
{ "batch": { "id": "…", "status": "partially_completed",
    "pieces": { "total": 1198, "created": 1198, "mailed": 1190, "failed": 8 },
    "pricing": { "total_cents": 42050, "credits_charged": true, "refunded_cents": 280 },
    "facility_notes": "8 returned by USPS as undeliverable" },
  "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.

JSON · response
{ "id": "…", "status": "cancelled", "refunded_credits_cents": 42050, "sandbox": false }
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. Sandbox keys: no charge; then poll until submitted and use /advance.

HTTP · request and response
POST /api/v1/batch-mail/{id}/confirm
Authorization: Bearer sk_agent_...
Idempotency-Key: q2-recall-confirm-0001

200 { "batch": { "status": "paid", "pricing": { "total_cents": 42050, "credits_charged": true } },
      "credits_charged_cents": 42050, "credit_balance_cents": 157950, "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 mail requires $420.50 and your balance is $12.00.", "code": "INSUFFICIENT_CREDITS",
      "billing_url": "https://mailbox.bot/dashboard/billing#credits", "credits_required_cents": 42050, "credit_balance_cents": 1200, "credit_shortfall_cents": 40850 }
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. Live batches are advanced by the facility, never by the API.

JSON · response
{ "batch": { "status": "in_progress", "test_mode": true, "pieces": { "total": 2, "created": 2, "mailed": 0, "failed": 0 } },
  "previous_status": "submitted", "sandbox": true }

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

Batch events go to your agent's webhook URL with the same signing and retry rules as outbound mail events (see webhooks overview).

EventMeaning
batch.submittedPaid, every piece generated, in the facility's print queue.
batch.completedThe facility mailed every piece.
batch.partially_completedThe facility mailed some pieces; credits for the rest are refunded (pricing.refunded_cents).
batch.failedThe facility could not fulfill the batch; credits are refunded in full.
batch.cancelledCancelled before printing; credits are refunded.
JSON · example event
{
  "event_type": "batch.partially_completed",
  "batch_job_id": "00000000-0000-4000-8000-000000000010",
  "job_name": "Q2 Recall Notices",
  "mail_type": "letter",
  "mailing_class": "first_class",
  "total_pieces": 1198,
  "pieces_mailed": 1190,
  "pieces_failed": 8,
  "total_cents": 42050,
  "status": "partially_completed",
  "error_message": "8 returned by USPS as undeliverable",
  "created_at": "2026-09-25T15:10:00Z",
  "completed_at": "2026-09-26T18:02:11Z"
}

Errors

codeMeaning
BATCH_TEMPLATE_PAGES_INVALID400 — the PDF's page count does not fit the mail type (see Pricing). Pick another type, turn on duplex, or change the PDF.
BATCH_MARKETING_MAIL_MINIMUM400 — Marketing Mail needs at least 200 valid recipients. Use first_class or add recipients.
INSUFFICIENT_CREDITS402 — carries billing_url and the shortfall. Add credits, then confirm again with a new Idempotency-Key.
BATCH_GENERATION_DISPATCH_PENDING503 — the batch is funded; generation is being retried automatically. Poll the batch; it moves to submitted on its own.
BATCH_MAIL_NOT_CANCELLABLE409 — the facility has started (in_progress) or the batch is already terminal.
BATCH_ADVANCE_INVALID409 — sandbox only: still generating, or already completed.
MAILBOX_MD_VERSION_MISMATCH409 — agent keys send the current X-Mailbox-MD-Version, as on POST /v1/mail.

API reference · OpenAPI schema · Webhooks guide