LetterAgent API docs


Note: LetterAgent is in private testing (see pricing). The API is live at https://api.getletteragent.com/v1 and the MCP server at https://api.getletteragent.com/mcp. Examples below use these hosts. as a placeholder.

Basics


Endpoints

Method & pathWhat it does
POST /v1/quotesExact per-letter quote. Final price — no hidden fees.
POST /v1/jobsCreate a letter job from a quote. Returns the job and its one-tap Stripe Checkout URL.
POST /v1/webhooks/stripeStripe webhook. On checkout.session.completed the job is fulfilled (the letter is created with the print partner).
GET /v1/jobs/{job_id}Job status, amount, and live mail status.
GET /v1/letters/{letter_id}Raw print-partner letter record.

1. Quote a letter

POST /v1/quotes
Content-Type: application/json

{ "country": "CA", "color": false, "province_or_state": "ON",
  "text": "Dear landlord, please fix the tap. — Sam" }

Response 201:

{
  "quote_id": "q_fa6f8c0034374ea4",
  "currency": "CAD",
  "country": "CA",
  "color": false,
  "pages": 1,
  "registered": false,
  "amount_cents": 615,
  "amount": "C$6.15",
  "tax_cents": 80,
  "tax_label": "HST (13%)",
  "total_cents": 695,
  "total": "C$6.95",
  "content_hash": "9f2c…e4a1"
}

Pass the letter content — exactly one of html, text, or pdf_url. The backend renders it in the standard print layout, verifies the real page count, and returns the exact price — never an estimate. The quote binds the price to that exact content. Set registered to true for registered mail (US: certified with tracking; CA: registered with signature). Quote the exact amount_cents to the user and get explicit approval before creating the job. Published retail prices: US B&W $2.12, US colour $2.44, +$0.20/+$0.40 per extra page (USD), US registered B&W $10.40 (+$0.13/+$0.26 per extra registered page); Canada B&W C$6.15, Canada colour C$6.44, +C$0.58/+C$0.87 per extra page (CAD), Canada registered B&W C$51.12 (+C$0.38/+C$0.57 per extra registered page). Canadian prices exclude tax: pass province_or_state (e.g. "ON") and the quote also returns tax_cents, tax_label and total_cents — the sales tax added on top at checkout (HST 13% ON / 15% NB·NL·PE / 14% NS, GST 5% elsewhere, GST+QST 14.975% QC; US orders are not taxed).

Physical format: 8.5″ × 11″ paper, printed single-sided, recipient and sender addresses at the top of the first page, mailed in a standard double-window envelope with the sender address as the return address. Full details: what the recipient receives.


2. Create a job (after the user approves the price)

POST /v1/jobs
Content-Type: application/json

{
  "quote_id": "q_fa6f8c0034374ea4",
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440000",
  "letter": {
    "to": {
      "firstName": "Jane", "lastName": "Doe",
      "addressLine1": "123 Main St", "city": "Toronto",
      "provinceOrState": "ON", "postalOrZip": "M4B 1B3",
      "countryCode": "CA"
    },
    "from": {
      "companyName": "Sam Smith",
      "addressLine1": "456 Oak Ave", "city": "Markham",
      "provinceOrState": "ON", "postalOrZip": "L6B 0P6",
      "countryCode": "CA"
    },
    "html": "<html><body><p>Dear landlord, please fix the tap. — Sam</p></body></html>"
  }
}

Response 201 (or 200 with the identical body if the idempotency_key was already used — no duplicate job, no second charge):

{
  "job_id": "job_8f42k1ab9c0d2e3f",
  "status": "awaiting_payment",
  "amount_cents": 615,
  "amount": "C$6.15",
  "tax_cents": 80,
  "tax_label": "HST (13%)",
  "total_cents": 695,
  "total": "C$6.95",
  "currency": "CAD",
  "pages": 1,
  "payment_status": "pending",
  "checkout_url": "https://checkout.stripe.com/c/...",
  "payment_mode": "real"
}

to/from require addressLine1, city, countryCode (US/CA), and a name: firstName (+ optional lastName), companyName, or the convenience name field ("Jane Smith" is split into first/last automatically). A contact with no name is rejected with 400 before any payment is created. Send the same content the quote was created from — exactly one of html, text, or pdf_url. The quote's country must match the destination country. Content binding: the quote prices the exact content it received. The backend re-verifies the content before creating anything — if it differs, the call fails with 400 and code: "quote_content_mismatch", and no job and no payment session are created. Get a fresh quote and re-confirm the new price with the user instead: a price the user did not approve is never charged. Send the letter body as html and keep the top ~3 inches of the first page free (the recipient address block is overlaid there); your own page CSS is ignored. Hand checkout_url to the user — one tap, one card payment, one letter. The checkout shows the quote currency (USD for US letters, CAD for Canadian letters). For Canadian letters the tax is computed from the recipient's provinceOrState and appears as its own line item on the Stripe Checkout page — the customer pays total_cents (amount_cents + tax_cents).


3. Payment webhook

Point Stripe at POST /v1/webhooks/stripe. On checkout.session.completed the server marks the job paid and creates the letter with the print partner — fulfilment starts only after payment succeeds. Duplicate deliveries return the already-created letter instead of mailing twice.


4. Check status / cancel

GET /v1/jobs/job_8f42k1ab9c0d2e3f

{
  "job_id": "job_8f42k1ab9c0d2e3f",
  "status": "fulfilled",
  "amount_cents": 615,
  "amount": "C$6.15",
  "currency": "CAD",
  "letter_id": "letter_v3cwk7Nwm13XRGHwkSzSsE",
  "payment_status": "paid",
  "fulfillment_status": "in_production",
  "delivery_status": "untracked",
  "error": null
}

Job status: awaiting_payment → paid → fulfilled (or failed/cancelled). payment_status is pending | paid | failed; fulfillment_status is not_started | queued | in_production | mailed | failed | cancelled. mailed means the letter left the print facility — standard mail is untracked, so never report confirmed delivery. Registered mail is different: delivery_status becomes awaiting_tracking once mailed, in_transit when the carrier tracking number is available (a few days after mailing — test mode issues no tracking), and delivered only on explicit carrier delivery evidence. The job view then includes tracking_number, tracking_url and tracking_carrier — hand the tracking number to the customer in chat so they can follow it on the carrier's site.


5. Errors

All errors look like this:

{ "error": "quote is for US but destination is CA; request a new quote" }
HTTPMeaning
400Bad request — fix the fields named in the message and retry.
401Webhook signature check failed (real-Stripe mode only).
404Unknown job, letter id, or endpoint ({ "error": "not_found" } for unknown endpoints).
409Job is in a state that can't be fulfilled (e.g. already failed).
502The print partner call failed; the job is marked failed with details.

6. MCP server

The same four operations are live as a real MCP server (Model Context Protocol, Streamable HTTP, stateless) — point any MCP-compatible assistant at:

https://api.getletteragent.com/mcp

It speaks JSON-RPC 2.0: initialize, tools/list, tools/call for quote_letter, create_letter_job, get_job_status, and cancel_letter. Behaviour is identical to the REST API — same content-verified pricing, same idempotency, same cancellation, same pricing. mcp-tools.json mirrors the tool shapes for reference. Tool errors come back as isError results, not HTTP errors.


7. Security model

LetterAgent is account-free by design, not by omission:

The residual risk is nuisance-level (job-creation spam), not theft-level. Adding passwords or API keys would weaken the core pitch — "no signup, no API key" — without buying real security, so the design stays keyless.


8. Mail a check

Checks ride the same flow as letters — quote, approve, pay, fulfil, receipt — on parallel endpoints. One flat price whatever the face amount: $5.00 USD / C$7.00 CAD + tax ($5.00 USD for US payees, C$7.00 CAD for Canadian payees; Canadian sales tax is added on top at checkout exactly like letters).

POST /v1/check-quotes
Content-Type: application/json

{ "country": "US" }

Response 201:

{
  "service": "check",
  "quote_id": "q_fa6f8c0034374ea4",
  "currency": "USD",
  "country": "US",
  "amount_cents": 500,
  "amount": "$5.00"
}

Pass province_or_state (e.g. "ON") for Canadian payees and the quote also returns tax_cents, tax_label and total_cents. State the payee, the check face amount, and the exact total to the user and get explicit approval before creating the job — a check is a money movement and cannot be edited afterwards.

POST /v1/check-jobs
Content-Type: application/json

{
  "quote_id": "q_fa6f8c0034374ea4",
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440000",
  "check": {
    "to": {
      "firstName": "Jane", "lastName": "Doe",
      "addressLine1": "123 Main St", "city": "Buffalo",
      "provinceOrState": "NY", "postalOrZip": "14201",
      "countryCode": "US"
    },
    "from": {
      "companyName": "Sam Smith",
      "addressLine1": "456 Oak Ave", "city": "Austin",
      "provinceOrState": "TX", "postalOrZip": "73301",
      "countryCode": "US"
    },
    "bank_account": "bank_8f42k1ab9c0d2e3f",
    "amount_cents": 4242,
    "memo": "October rent"
  }
}

to is the payee; from is the sender (its address is the return address). bank_account is the pre-registered bank account ID the check is drawn on. The sender registers the account once, either on the bank account registration page or via POST /v1/bank-accounts (see below); afterwards the API references it by ID only. Raw routing/account numbers are never accepted (a 400 rejects them). amount_cents is the face amount in cents: $1.00 minimum, $25,000.00 maximum per check. memo is optional (max 40 characters, printed on the check); an optional message prints an HTML note on the same page as the check. The quote's country must match the payee's country.

Registering a bank account. POST /v1/bank-accounts accepts the account details exactly once, over TLS, and returns the bank account ID:

POST /v1/bank-accounts
Content-Type: application/json

{
  "nickname": "TD chequing",
  "country": "CA",
  "account_holder_name": "Sam Smith",
  "bank_name": "TD Canada Trust",
  "institution_number": "004",
  "transit_number": "12345",
  "account_number": "1234567"
}

For US accounts, send routing_number (9 digits, checksum-verified) instead of the institution/transit pair; Canadian accounts may also send a single routing_number as 8 digits ("004-12345"). account_type (chequing/savings) is optional and for the user's own records. The numbers are forwarded to the check-printing provider a single time and are never logged, stored, or returned: the response is 201 with only {"bank_account_id": "bank_...", "nickname": "..."}. The endpoint is rate-limited (10 per hour per IP). Prefer the registration page for humans; the endpoint exists for programmatic use. Raw numbers must never be typed into chat.

The response mirrors a letter job (job_id, status, payment_status, checkout_url or pay link, tax_cents/total_cents for Canada) with "service": "check", plus check_amount_cents and check_amount (the face amount). The payment webhook, pay page (/pay/<code>), status (GET /v1/jobs/<id>), receipt (GET /v1/jobs/<id>/receipt), and cancel endpoints all work for check jobs: the status view carries check_id instead of letter_id and delivery_status is always untracked — checks travel as standard mail. The proof-of-mailing receipt names the payee and face amount and fingerprints the exact check instruction; it never confirms delivery or clearance. Full details: mail a check.

MCP tools: quote_check and create_check_job (same approval contract as create_letter_job). get_job_status, get_proof_of_mailing, and cancel_letter work for check jobs via job_id.


9. Send a fax

Faxes ride the same flow as letters — quote, approve, pay, send — on parallel endpoints. Send-only: there is no inbound fax. One flat per-page price, $2.00 USD for the first page plus $1.00 USD for each additional page, USD only: Canadian destinations are billed in USD too; Canadian sales tax is added on top at checkout, computed from the destination province like letters. US and Canadian fax numbers only; 200-page cap.

POST /v1/fax-quotes
Content-Type: application/json

{ "country": "US",
  "text": "Dear billing, please see the attached. — Sam" }

Response 201:

{
  "service": "fax",
  "quote_id": "q_fa6f8c0034374ea4",
  "currency": "USD",
  "country": "US",
  "pages": 2,
  "amount_cents": 300,
  "amount": "$3.00"
}

Pass the fax content — exactly one of html, text, or pdf_url. The backend renders it in the fax layout, verifies the real page count, and returns the exact price — never an estimate. The quote binds the price to that exact content. Pass province_or_state (e.g. "ON") to record the destination region. State the destination fax number and the exact total to the user and get explicit approval before creating the job — a fax is a money movement and a sent fax cannot be unsent.

POST /v1/fax-jobs
Content-Type: application/json

{
  "quote_id": "q_fa6f8c0034374ea4",
  "idempotency_key": "550e8400-e29b-41d4-a716-446655440000",
  "fax": {
    "to_number": "+14165551234",
    "province_or_state": "ON",
    "text": "Dear billing, please see the attached. — Sam"
  }
}

to_number is the destination fax number in E.164 format (US or CA only). Send the same content the quote was created from — exactly one of html, text, or pdf_url. The quote's country must match the fax number's country. Content binding: the backend re-verifies the content before creating anything — if it differs, the call fails with 400 and code: "quote_content_mismatch", and no job and no payment session are created. A price the user did not approve is never charged. Retrying with the same idempotency_key returns the original job instead of creating a duplicate.

The response mirrors a letter job (job_id, status, payment_status, checkout_url or pay link) with "service": "fax", plus fax_number and pages. The fax is sent only after payment completes. Fax jobs move through queued → sending → sent (or failed); the status view (GET /v1/jobs/<id>) carries fax_id instead of letter_id. The pay page (/pay/<code>) shows exactly what will be faxed before the customer pays; the proof-of-sending receipt (GET /v1/jobs/<id>/receipt) fingerprints the exact document. In test mode no real fax is sent.

MCP tools: quote_fax and create_fax_job (same approval contract as create_check_job — state the destination number and the exact price, get explicit approval before creating).


Home · Contact: hello@getletteragent.com · No cookies. No tracking.