Mail API

Send real physical mail with a single API call. You supply the recipient and artwork URLs; we validate the address, quality-check and normalize the artwork, print, and mail. The API is product-based — 4"×6" postcards today, with more sizes and formats (larger postcards, letters) on the way; postal.get_specs always lists what's live on your account, so build against it rather than hard-coding a product.

Authentication

Every request is a POST to https://www.posttimely.com/api/v1/{tool} with a JSON body and your API key as a bearer token. Getting a key is self-serve: sign up dashboard → API Keys → Generate Key. The raw key is shown once. Keys never change mode: sandbox keys are pt_test_ forever, and going live issues a second, separate pt_live_ key — both work side by side.

curl -X POST https://www.posttimely.com/api/v1/postal.get_specs \
  -H "Authorization: Bearer pt_test_..." \
  -H "Content-Type: application/json" \
  -d '{}'

Sandbox vs live

Keys come in two modes. Test keys (pt_test_) exercise the entire real pipeline — address validation, artwork QC, even the print provider in test mode — and return a genuine proof PDF, but nothing is printed, mailed, or billed. Live keys (pt_live_) print real mail and bill your account per piece. Integrate against a test key first; switching to live is changing one string.

Sandbox limits: test pieces are unlimited and free, never count toward billing or volume tiers (they're reported separately in postal.get_usage), and skip the hold window so your proof PDF comes back on the send response. The only cap is the per-key rate limit (see Errors & rate limits). Pricing behaves identically in sandbox — quotes and per-piece prices come from your real account rates, so the numbers you see in testing are the numbers you'll be billed live.

Going live is self-serve: dashboard → API keys → Go live — a one-time $5 activation fee verifies your card (which stays on file for monthly usage billing) and your pt_live_ key is created on the spot. Every live send re-verifies the card on file — without one the API returns PAYMENT_METHOD_REQUIRED (HTTP 402) and nothing is accepted or printed. Your integration doesn't change: going live is swapping pt_test_ for pt_live_ in one place — and switching back is just as easy: your test key keeps working for free sandbox sends, side by side with live, forever.

Quickstart

Four calls take you from nothing to a proof PDF:

  1. postal.get_specs — artwork dimensions and rules.
  2. postal.validate_address — optional pre-check; verified addresses get the lower rate.
  3. postal.send_postcard — the send. Returns the piece with a proof PDF (sandbox) or a cancel-by time (live).
  4. postal.get_mailing — status and proof any time after.

Sending a postcard

Artwork: landscape JPG, PNG, or print-ready PDF via public URL. Ideal size is 1871×1271px (300dpi with 3mm bleed, 3:2). Images from 1200×800 up are auto-fitted and upscaled; keep text 71px from every edge. On the back, the bottom-right 1157×796px is the reserved postal area — we overlay it white for the address, postage, and barcode.

Front-only sends (recommended): omit back_url and we generate a compliant back for you. The front is entirely yours — full bleed, no postal elements — so per-recipient unique artwork lives there with nothing to work around. Supply your own back only if you want branding on it, and keep the reserved area clear.

curl -X POST https://www.posttimely.com/api/v1/postal.send_postcard \
  -H "Authorization: Bearer pt_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "recipient": {
      "firstname": "Jane", "lastname": "Doe",
      "address1": "100 Main St W", "city": "Hamilton",
      "region": "ON", "postcode": "L8P 1H6", "country": "CA"
    },
    "front_url": "https://example.com/front.jpg",
    "back_url": "https://example.com/back.jpg",
    "idempotency_key": "order-8412-postcard-1"
  }'

Response (abbreviated):

{
  "data": {
    "id": "6f1c9f9e-...",
    "status": "held",
    "verified": true,
    "unit_price_cents": 92,
    "currency": "USD",
    "cancel_by": "2026-08-25T14:00:00Z",
    "proof_pdf_url": null,
    "warnings": ["front artwork was upscaled from 1536x1024 ..."]
  }
}

Pass an idempotency_key on every send — retries with the same key return the original piece (flagged idempotent_replay: true) instead of mailing twice. Use deterministic keys (e.g. a hash of your order id + recipient) so an interrupted batch can be blindly re-run. Keys are scoped per mode — reusing a key from sandbox testing never blocks the real live send — and a failed piece releases its key so the same retry creates a fresh attempt.

The cancel window

Live sends are held for 24 hours before going to print (configurable per send with hold_hours, 0–72; sandbox sends skip the hold so you get your proof immediately). While held, cancellation is instant and guaranteed:

curl -X POST https://www.posttimely.com/api/v1/postal.cancel_mailing \
  -H "Authorization: Bearer pt_live_..." \
  -H "Content-Type: application/json" \
  -d '{"mailing_id": "6f1c9f9e-..."}'

After the hold, the piece goes to production. Cancellation is then best-effort — it succeeds only if the piece hasn't started printing, and a TOO_LATE_TO_CANCEL error means it's on its way. Cancelled pieces are never billed. Sending with "hold_hours": 0 mails as fast as possible in exchange for giving up the guaranteed window.

Statuses

  • held — accepted and priced; in your cancel window, not yet at the printer. A transient print-submission problem can also return a piece here briefly (its error field says so) while we retry automatically; it only becomes failed after repeated attempts.
  • submitted / received / producing — at the print facility.
  • dispatched — handed to the postal service.
  • delivered — estimated delivered (postal-service estimate, not a signature).
  • cancelled — cancelled; never billed.
  • failed — could not be produced; never billed. See error.
  • test — sandbox piece; proof generated, nothing mailed.

Pricing & billing

Per-piece pricing with two rates: verified — the address passed our validation (existence and deliverability checked, address normalized) — and unverified — it didn't, and we mail it as supplied at the higher rate. Each piece is classified at send time; postal.validate_address lets you pre-check and clean addresses so more of your pieces earn the verified rate. Postal codes must be well-formed for the destination country — an unparseable postcode is rejected (INVALID_POSTCODE) rather than mailed, since it can't be delivered as addressed. Your exact rates: postal.get_quote. Month-to-date: postal.get_usage. Billing is a single automatic invoice at month end.

Rates are per product and monthly-volume tiered, retroactive — your whole month bills at the bracket you reach. Standard self-serve rates for the 4×6 postcard (per piece, USD): 1–4 pieces/mo $1.80 verified / $1.98 unverified; 5–99 $1.01 / $1.19; 100+ $0.96 / $1.14. New sizes and formats get their own rate tables as they launch. Sending more? Talk to us about custom volume pricing — postal.get_quote always shows the rates live on your account (marked standard or custom).

Errors & rate limits

Errors return a machine-readable code — notably ARTWORK_TOO_SMALL, PORTRAIT_ARTWORK, ARTWORK_FETCH_FAILED, INVALID_RECIPIENT, INVALID_POSTCODE, COUNTRY_NOT_SUPPORTED, PAYMENT_METHOD_REQUIRED, TOO_LATE_TO_CANCEL. Rate limits are per key, returned in X-RateLimit-* headers — self-serve sandbox keys allow 30 requests/minute, live keys 120/minute (need more? ask us). A 429 means back off until the reset time. Successful proof PDF links are permanent — safe to store and share.

Questions or higher limits: support@posttimely.com