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:
postal.get_specs— artwork dimensions and rules.postal.validate_address— optional pre-check; verified addresses get the lower rate.postal.send_postcard— the send. Returns the piece with a proof PDF (sandbox) or a cancel-by time (live).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
errorfield 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.