Mail A Letter API

Send physical mail with an HTTP request. Upload a PDF, give us the recipient's address, and we print, stuff, stamp, and mail it. Postage and printing are billed from your prepaid MailBank balance — no card details ever touch the API.

Downloads: OpenAPI 3.0 specification  |  Postman collection

Getting started

  1. Sign in and open your Account page.
  2. Set Enable API to Yes and save. You get two keys: a production key (mal_live_…) that charges your MailBank and mails real letters, and a sandbox key (mal_test_…) for integration testing.
  3. Add funds to your MailBank before going live.

Authenticate every request with your key in the Authorization header (or an X-Api-Key header). Base URL: https://www.mailaletter.com/api/v1

curl https://www.mailaletter.com/api/v1/balance \
  -H "Authorization: Bearer mal_test_YOUR_SANDBOX_KEY"

Send a letter

POST /api/v1/letters — the file must be a PDF (20MB max, US Letter or A4, portrait or landscape), base64-encoded. Password-protected PDFs are rejected. Recommended: 0.5in margins minimum.

curl https://www.mailaletter.com/api/v1/letters \
  -H "Authorization: Bearer mal_test_YOUR_SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "file_name": "welcome.pdf",
    "file": "'"$(base64 < welcome.pdf | tr -d '\n')"'",
    "recipient": {
      "first_name": "Jane",
      "last_name": "Doe",
      "company": "Acme Corp",
      "address1": "500 Union St",
      "address2": "Suite 100",
      "city": "Seattle",
      "state": "WA",
      "postal_code": "98101",
      "country": "US"
    },
    "options": {
      "return_envelope": false,
      "delivery_service": "first_class"
    }
  }'
FieldRequiredNotes
file_nameYesMust end in .pdf
fileYesBase64-encoded PDF, 20MB max
recipient.first_name / last_nameYes*Combined max 40 characters. *Optional when company is given
recipient.companyNo*Max 40 characters. *Required when no name is given
recipient.address1YesStreet address
recipient.address2NoApartment, suite, unit. Two street lines is the limit — there is no address3
recipient.cityYes
recipient.stateUS & Canada2-letter state for US addresses; province for Canada
recipient.postal_codeUS & Canada
recipient.countryYes2-letter ISO code (e.g. US) or full name
options.return_envelopeNoInclude a self-addressed return envelope. Defaults to false
options.delivery_serviceNoOne of first_class (default), tracking, certified, restricted_delivery, express. Exactly one service per letter. tracking, certified and restricted_delivery are US destinations only — requesting them for an international address is a 400

Successful response (201):

{
  "id": "1024042",
  "test_mode": false,
  "status": "processing",
  "pages": 2,
  "cost": 1.87,
  "tax": 0.00,
  "total": 1.87,
  "balance_remaining": 48.13
}

Washington state destinations add 9.5% sales tax.

If your MailBank cannot cover the letter you get 402 with your balance and the amount required; the letter is saved as a draft you can pay for on the website after topping up. Nothing is mailed until it is paid. We also email your low-funds address (configurable on the Account page) — at most one email per day, no matter how many requests are declined.

Track your letters

GET /api/v1/letters/{id} — status is one of processing, address_verification, printing, printed, awaiting_delivery, mailed, canceled, errored, draft, pending_payment.

{
  "id": "1024042",
  "test_mode": false,
  "status": "mailed",
  "pages": 2,
  "cost": 1.87,
  "tax": 0.00,
  "total": 1.87,
  "fulfillment_provider": "USPS",
  "tracking_number": "9407300000000000000001"
}

fulfillment_provider appears once the letter is in the carrier pipeline (printing onward). tracking_number appears only for letters mailed with a delivery_service other than first_class — standard first-class letters do not carry one.

GET /api/v1/balance — your current MailBank balance.

{
  "balance": 48.13,
  "currency": "USD"
}

Test mode

Use your sandbox key and every call runs the complete pipeline — full PDF validation, address validation, and real pricing from your rate table — but nothing is stored, charged, or mailed. Responses have "test_mode": true and ids beginning test_. Test letters progress automatically so you can build polling: processingprinting after 5 minutes → mailed after 30 minutes.

Deterministic failure triggers (sandbox only):

TriggerResult
recipient postal_code = 00000400 invalid_address
recipient first_name = TEST-INSUFFICIENT-FUNDS402 insufficient_funds

Errors

All errors share one shape:

{ "error": { "code": "insufficient_funds", "message": "...", "balance": 0.42, "required": 1.87 } }
HTTPCodeMeaning
400invalid_json, invalid_requestMalformed body, missing fields, or an invalid options value
400invalid_pdf, encrypted_pdf, empty_pdf, unsupported_page_sizeThe file failed PDF validation
400invalid_addressRecipient address incomplete or country unknown
401unauthorizedMissing/unknown key, or API not enabled on the account
402insufficient_fundsMailBank balance too low; letter saved as draft
403https_requiredThe API is only available over HTTPS
404not_foundNo such letter on this account, or unknown API path
405method_not_allowedWrong HTTP method for the path; see the Allow header
413file_too_largePDF over 20MB
415unsupported_media_typeOnly PDF is accepted — file_name missing or not ending in .pdf
429rate_limitedOver 120 requests/minute; honor Retry-After
500internal_errorOur fault — contact support@mailaletter.com

Questions? Email support@mailaletter.com.