REST

API reference

Pricing: First-Class $1.49 page 1 + $0.25 extra pages; Certified $12.99 base + $0.25 extra pages; postcard $1.99 flat. Upload a PDF (JSON base64 or multipart) to preserve fonts.

Base path /api/v1. Authenticate with Authorization: Bearer lbl_live_... or lbl_test_.... Machine-readable spec: /api/v1/openapi.json.

Auth

  • Create keys in the workspace Developers page. Production keys start with lbl_live_. Staging keys start with lbl_test_ and never purchase postage.
  • Keys are hashed at rest and shown once.
  • Missing or invalid keys return HTTP 401 with { "error": "..." }.

curl · quote

curl -sS https://lettersbyletter.com/api/v1/pricing?mailClass=first_class&pageCount=2 \
  -H "Authorization: Bearer lbl_live_your_key"

Endpoints

  • POST /letters — create a mailing
  • GET /letters — list mailings
  • GET /letters/:id — status
  • POST /addresses/validate — address check
  • GET /pricing — quote
  • POST /credits — x402 credit purchase

Create a letter

Example only — these addresses are fictional placeholders for copy-paste. Replace every field before you mail.

If the workspace has credits, they are spent. Otherwise the API returns HTTP 402 with a PAYMENT-REQUIRED header. Retry with PAYMENT-SIGNATURE to pay per job in USDC, or top up with Square in the dashboard.

curl

curl -sS -X POST https://lettersbyletter.com/api/v1/letters \
  -H "Authorization: Bearer lbl_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "mailClass": "first_class",
    "from": {
      "name": "Your Name",
      "line1": "123 Example Street",
      "city": "New Haven",
      "state": "CT",
      "postal": "06510"
    },
    "recipients": [{
      "name": "Recipient Name",
      "line1": "456 Example Avenue",
      "city": "Austin",
      "state": "TX",
      "postal": "78701"
    }],
    "document": {
      "kind": "text",
      "content": "Hello from the LettersByLetter API."
    }
  }'

TypeScript

const key = process.env.LETTERSBYLETTER_API_KEY!; // lbl_live_... or lbl_test_...

export async function sendLetter() {
  const res = await fetch("https://lettersbyletter.com/api/v1/letters", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${key}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      mailClass: "first_class",
      from: {
        name: "Your Name",
        line1: "123 Example Street",
        city: "New Haven",
        state: "CT",
        postal: "06510",
      },
      recipients: [
        {
          name: "Recipient Name",
          line1: "456 Example Avenue",
          city: "Austin",
          state: "TX",
          postal: "78701",
        },
      ],
      document: { kind: "text", content: "Hello from the API." },
    }),
  });

  if (res.status === 402) {
    const challenge = res.headers.get("PAYMENT-REQUIRED");
    throw new Error(`Payment required. Header: ${challenge}`);
  }
  if (!res.ok) throw new Error(await res.text());
  return res.json();
}

Webhooks

Set a destination on the Developers page. We POST JSON on letter.created and credits.purchased. Verify X-LettersByLetter-Signature: sha256=<hmac> using your org webhook secret (or org id if you have not set a secret).

payload

{
  "id": "evt_...",
  "type": "letter.created",
  "createdAt": "2026-09-09T00:00:00.000Z",
  "data": {
    "id": "job_...",
    "status": "queued",
    "mailClass": "first_class",
    "priceCents": 149,
    "trackingNumber": null
  }
}

Error codes

  • 400 — validation or malformed body
  • 401 — missing/invalid API key or session
  • 402payment_required / insufficient_credits
  • 403 — admin-only route
  • 404 — letter not found
  • 422 — address validation failed