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 withlbl_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 body401— missing/invalid API key or session402—payment_required/insufficient_credits403— admin-only route404— letter not found422— address validation failed