Developers

Send letters from your software. One API call.

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.

Get started in three steps.

  1. Buy letters.

    Packs are on your account page, from $1.75 a letter. They never expire.

  2. Make an API key.

    On the same page. Make a test key too: it checks everything and mails nothing.

  3. Send a letter.

    One request. We print it, mail it USPS First-Class or Certified, and send your webhook when it’s mailed.

Letter

1

prepaid letter, up to 5 pages

  • Folded into a standard envelope
  • USPS First-Class Mail
  • A webhook the day it’s mailed

Certified letter

1

prepaid Certified letter, up to 5 pages

  • Folded into a standard envelope
  • USPS Certified Mail
  • Tracking number, and a webhook when it’s delivered

Black and white, to US addresses. Coming later: color, Certified with a return receipt, postcards, and flat & tracked mail.

Send a letter.

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.

Send a list.

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"}
    ]
  }'

Webhooks.

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.

Webhook events
EventWhen
letter.createdA letter was sent with your account's letters (from the API or your account page)
letter.mailedUSPS has it
letter.deliveredIt was delivered (Certified letters, which are tracked)
letter.address_problemUSPS couldn't match the address, so it's waiting. Nothing was mailed.
letter.returnedThe carrier is returning it to us
letter.canceledIt was canceled before it went to print
letter.refundedIts 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.

Test mode.

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.

Reference.

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.

Endpoints
EndpointWhat it does
GET/v1/meYour account, your letters left, this key
POST/v1/lettersSend one letter: text, or your own PDF
GET/v1/lettersYour 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}/cancelCancel it before it goes to print. Its letter comes back.
POST/v1/listsSend up to 400 letters at once
GET/v1/templatesYour saved letters
POST/v1/templatesSave 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/catalogThe letter types and handwriting styles for a brand
GET/v1/lists/{id}A list and where its letters are
GET/v1/webhook_endpointsYour webhook endpoints
POST/v1/webhook_endpointsAdd one (its secret is shown once)
DELETE/v1/webhook_endpoints/{id}Remove one
POST/v1/webhook_endpoints/{id}/roll_secretA new signing secret
POST/v1/test/letters/{id}/advanceMove a test letter to its next step

Errors

Errors look like {"error": {"code": "...", "message": "..."}}.

Errors
400Something in the request can't be used. error.code and error.param say what. Nothing was spent.
401No API key, or one that doesn't work
402You're out of letters. error.buy_url is your account page, where the packs are.
403Your account is on hold
409That Idempotency-Key was used for a different request, or the letter can't be canceled now
429Too many requests. Wait for Retry-After seconds.

Limits

Requests
120 a minute and 2,000 an hour for each key
Letters
60 an hour and 400 a day for each account. For more at once, send a list.
Lists
Up to 400 letters each
PDFs
1 to 5 pages, up to 4 MB
Keys and endpoints
Up to 5 keys and 25 webhook endpoints for each account

Building something?

Tell us what you’re connecting. Zapier and n8n are next.

Email us

contact@haveitprinted.com
Have It Printed, LLC · 1308 E Colorado Blvd, #3278, Pasadena, CA 91106