https://api.getletteragent.com/v1 and the MCP server at
https://api.getletteragent.com/mcp. Examples below use these hosts.
as a placeholder.
https://api.getletteragent.com/v1"idempotency_key": "<uuid>" in the POST /jobs body — one fresh UUID per user-confirmed action. Retrying with the same key returns the original job (200) instead of creating a duplicate (201). The server also attaches its own per-job Idempotency-Key to the print-fulfilment call, and the payment webhook is safe to receive twice.| Method & path | What it does |
|---|---|
| POST /v1/quotes | Exact per-letter quote. Final price — no hidden fees. |
| POST /v1/jobs | Create a letter job from a quote. Returns the job and its one-tap Stripe Checkout URL. |
| POST /v1/webhooks/stripe | Stripe 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. |
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.
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).
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.
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.
All errors look like this:
{ "error": "quote is for US but destination is CA; request a new quote" }
| HTTP | Meaning |
|---|---|
| 400 | Bad request — fix the fields named in the message and retry. |
| 401 | Webhook signature check failed (real-Stripe mode only). |
| 404 | Unknown job, letter id, or endpoint ({ "error": "not_found" } for unknown endpoints). |
| 409 | Job is in a state that can't be fulfilled (e.g. already failed). |
| 502 | The print partner call failed; the job is marked failed with details. |
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.
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.
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.
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.