Letter
1
prepaid letter, up to 5 pages
- Folded into a standard envelope
- USPS First-Class Mail
- A webhook the day it’s mailed
Developers
Mail a thank-you the moment a deal closes, a note when a car is delivered, a reminder on a date. We print it, mail it and tell you where it is. It’s paid with the prepaid letters in your account.
Packs are on your account page, from $1.75 a letter. They never expire.
On the same page. Make a test key too: it checks everything and mails nothing.
One request. We print it, mail it USPS First-Class or Certified, and send your webhook when it’s mailed.
1
prepaid letter, up to 5 pages
1
prepaid Certified letter, up to 5 pages
Black and white, to US addresses. Coming later: color, Certified with a return receipt, postcards, and flat & tracked mail.
Send your words as text and we print them in the handwriting-style font you choose, on your saved letterhead (your logo, footer and return address) unless you send "letterhead": false. We mail it to the address exactly as you send it.
curl https://agents.haveitprinted.com/v1/letters \
-H "Authorization: Bearer hipacct_live_..." \
-H "Idempotency-Key: deal-1001-thank-you" \
-H "Content-Type: application/json" \
-d '{
"to": {"name": "Rae Lopez", "line1": "1 Main St", "city": "Austin",
"state": "TX", "postal_code": "78701"},
"text": "Congratulations on your new home, Rae. It was a joy to help.",
"from": {"name": "Pat Rivera"},
"metadata": {"deal_id": "1001"}
}'The answer
{
"object": "letter",
"id": "SI-20261016-7F3A91C2",
"livemode": true,
"product": "letter",
"status": "processing",
"status_text": "Going to print",
"to": {"name": "Rae Lopez", "city": "Austin", "state": "TX"},
"metadata": {"deal_id": "1001"},
"cancelable": true,
"tracking_number": null
}Every POST needs an Idempotency-Key. Retry with the same key and you get the first answer back, so a letter is never sent twice. Use "product": "certified" for Certified Mail, and brand to choose whose name is on the emails about it.
A PDF, as Certified Mail
curl https://agents.haveitprinted.com/v1/letters \
-H "Authorization: Bearer hipacct_live_..." \
-H "Idempotency-Key: lease-renewal-2026-10" \
-F 'json={"to": {"name": "Sam Ortiz", "line1": "22 Elm St", "city": "Dallas",
"state": "TX", "postal_code": "75201"}, "product": "certified"}' \
-F "pdf=@renewal.pdf" Save a letter once, on your account page or with POST /v1/templates, with blanks like {{first_name}}. Then send it by name: "template": "Closing thank-you" and "merge": {"first_name": "Rae"}. If a blank is missing, we name it and send nothing.
Add "dry_run": true to any send to check it without sending it: you get the preview and what it would take, and nothing is spent or mailed.
Your own PDF works too: 1 to 5 pages, up to 4 MB, as a file in a form like this, or base64 in pdf. A letter can be canceled for about 10 minutes, until it goes to print. Its letter comes right back to your account.
Up to 400 letters in one request, all of them or none. Each one is filled in with {{first_name}}, {{name}}, {{city}}, {{state}} or a column of your own, like {{closing_date}}. You can send a CSV or Excel file instead of rows. If a row can’t be mailed, we name it and spend nothing.
curl https://agents.haveitprinted.com/v1/lists \
-H "Authorization: Bearer hipacct_live_..." \
-H "Idempotency-Key: october-closings" \
-H "Content-Type: application/json" \
-d '{
"text": "Hi {{first_name}}, happy one-year anniversary in your home!",
"from": {"name": "Pat Rivera"},
"recipients": [
{"name": "Ana Lopez", "line1": "1 Oak St", "city": "Austin", "state": "TX", "postal_code": "78701"},
{"name": "Ben Kim", "line1": "2 Elm St", "city": "Dallas", "state": "TX", "postal_code": "75201"}
]
}'Add an HTTPS endpoint on your account page, or with POST /v1/webhook_endpoints. We send it each event as JSON, signed with your endpoint’s secret, which we show you once.
| Event | When |
|---|---|
letter.created | A letter was sent with your account's letters (from the API or your account page) |
letter.mailed | USPS has it |
letter.delivered | It was delivered (Certified letters, which are tracked) |
letter.address_problem | USPS couldn't match the address, so it's waiting. Nothing was mailed. |
letter.returned | The carrier is returning it to us |
letter.canceled | It was canceled before it went to print |
letter.refunded | Its letter went back to your account |
An event
{
"id": "evt_5c1e0d8a4b2f7a9e3d6c1b08",
"object": "event",
"type": "letter.mailed",
"created": 1792195200,
"livemode": true,
"data": {"object": {"object": "letter", "id": "SI-20261016-7F3A91C2",
"status": "mailed", "metadata": {"deal_id": "1001"}}}
}Check every delivery before you trust it. The HIP-Signature header is t=<unix seconds>,v1=<hex>, where v1 is the HMAC-SHA256 of t.body with your secret. After you roll a secret, both secrets sign for 24 hours.
Checking a signature in Python
import hashlib, hmac, time
def is_from_have_it_printed(secret, header, body, tolerance=300):
"""secret: your endpoint's whsec_... ; header: HIP-Signature ; body: the raw bytes."""
parts = [p.split("=", 1) for p in header.split(",")]
t = int(next(v for k, v in parts if k == "t"))
if abs(time.time() - t) > tolerance:
return False
want = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(want, v) for k, v in parts if k == "v1")And in Node.js
const crypto = require("crypto");
function isFromHaveItPrinted(secret, header, rawBody, tolerance = 300) {
const parts = header.split(",").map((p) => p.split("="));
const t = Number(parts.find(([k]) => k === "t")[1]);
if (Math.abs(Date.now() / 1000 - t) > tolerance) return false;
const want = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return parts.some(([k, v]) => k === "v1" && v.length === want.length &&
crypto.timingSafeEqual(Buffer.from(v), Buffer.from(want)));
}Answer with any 2xx within 10 seconds. We don’t follow redirects. If a delivery fails, we try again for about two and a half days. If your endpoint fails for 3 days, we switch it off and email you. Switching it back on is one tap.
A test key (hipacct_test_...) runs every check a real send does. It spends nothing and mails nothing, and its letters say "livemode": false. They move only when you move them, and each step sends its webhook to your test endpoints, for real.
curl -X POST https://agents.haveitprinted.com/v1/test/letters/test_4b4d95a1c2301eafe46f/advance \ -H "Authorization: Bearer hipacct_test_..." \ -H "Idempotency-Key: advance-1"
A test letter goes processing, printing, mailed, then delivered for Certified. Send {"to": "address_problem"} or {"to": "returned"} to try those too.
Everything is at https://agents.haveitprinted.com/v1, with your key as Authorization: Bearer .... The full OpenAPI 3.1 description is at agents.haveitprinted.com/v1/openapi.yaml.
| Endpoint | What it does |
|---|---|
GET/v1/me | Your account, your letters left, this key |
POST/v1/letters | Send one letter: text, or your own PDF |
GET/v1/letters | Your letters, oldest first after since, or newest first with order=desc |
GET/v1/letters/{id} | Where one letter is, with tracking for Certified |
POST/v1/letters/{id}/cancel | Cancel it before it goes to print. Its letter comes back. |
POST/v1/lists | Send up to 400 letters at once |
GET/v1/templates | Your saved letters |
POST/v1/templates | Save a letter, with blanks like {{first_name}} |
PATCH/v1/templates/{id} | Change a saved letter |
DELETE/v1/templates/{id} | Remove a saved letter |
GET/v1/catalog | The letter types and handwriting styles for a brand |
GET/v1/lists/{id} | A list and where its letters are |
GET/v1/webhook_endpoints | Your webhook endpoints |
POST/v1/webhook_endpoints | Add one (its secret is shown once) |
DELETE/v1/webhook_endpoints/{id} | Remove one |
POST/v1/webhook_endpoints/{id}/roll_secret | A new signing secret |
POST/v1/test/letters/{id}/advance | Move a test letter to its next step |
Errors look like {"error": {"code": "...", "message": "..."}}.
| 400 | Something in the request can't be used. error.code and error.param say what. Nothing was spent. |
|---|---|
| 401 | No API key, or one that doesn't work |
| 402 | You're out of letters. error.buy_url is your account page, where the packs are. |
| 403 | Your account is on hold |
| 409 | That Idempotency-Key was used for a different request, or the letter can't be canceled now |
| 429 | Too many requests. Wait for Retry-After seconds. |
Tell us what you’re connecting. Zapier and n8n are next.
Email uscontact@haveitprinted.com
Have It Printed, LLC · 1308 E Colorado Blvd, #3278, Pasadena, CA 91106