Send a PDF as postal mail on behalf of a user
Courio prints a PDF and mails it as a real letter, from a standard letter to registered mail with proof of delivery, sent as the destination country’s own postal product. You do not need an account, an API key or an installed plugin. You get a quote, your user approves and pays, the letter is mailed, and you can follow it.
Summary
- MCP endpoint
https://api.courio.si/mcp(streamable HTTP, no auth)- REST base URL
https://api.courio.si/v1- Plain-text summary
/llms.txt- Account
- None. Optional API key (
Authorization: Bearer ck_…) for history and higher caps. - Payment
- Stripe Checkout link for a human, or x402 (USDC on Base) for autonomous agents.
- Flow
coverage→quote→ show price to human →create letter→ pay →status
Pick your path
| If you can… | Use | Payment |
|---|---|---|
| Drive a web browser | /send-pdf: a plain HTML form with labelled fields. Upload the PDF, fill in both addresses, get the quote, tick the confirmation, open the payment link. | Hand the Stripe page to your user. |
| Call MCP tools | The hosted MCP server at https://api.courio.si/mcp, if your user has added it as a connector. | Show the returned Stripe link to your user. |
| Make HTTP requests | The REST API at https://api.courio.si/v1 (below). | Stripe link for your user, or x402 if you can sign payments and your user authorised it. |
Rules you must follow
- Show the human the price, the recipient and the document before payment.
Payment is the point of no return: the letter is mailed right after and cannot be cancelled or recalled.
- Over MCP you cannot pay.
send_letter returns a Stripe link; give it to the
human and ask them to open it.
- Send an
Idempotency-Key header on every REST POST.
Reuse the same key when you retry the same request (including the x402 retry after a 402).
- Reuse quotes. A quote is valid 24 h at a fixed price, and quotes are capped per day.
Don’t re-quote to “refresh” a price.
- Read the
hint. Every error is application/problem+json with a
stable code and a one-sentence hint telling you what to do next.
- Identify yourself with
X-Agent-Id: your-agent/version. Optional, but it
helps us support you.
Limits
- One letter per request. PDF up to 15 MB and 20 pages, not password-protected.
- Registered services exist only in the countries listed under Coverage; elsewhere only
standard. - A quote is valid 24 hours. An unpaid payment link expires after about 24 hours.
- Daily caps apply per IP, sender email, wallet and API key, and a single letter cannot cost more than €50. A cap answers
429 limit_exceeded or 422 amount_limit_exceeded. - No cancellation after payment. If a paid letter cannot be mailed, card payments are refunded automatically.
- Content is screened before the quote. Threats, harassment and impersonation are refused (
422 content_rejected); do not try to work around it.
Choosing a service
Ask for a generic level; Courio maps it to the legal product of the destination country (FR “Lettre
recommandée avec AR”, DE “Einschreiben Rückschein”, US “Certified Mail”…). The quote returns the exact
product in legal_product.
serviceProof Use it for standard none Ordinary mail. Replies, reminders, anything without legal weight. tracked none (tracking number) Delivery is tracked, no signature. registered deposit Proof the letter was handed to the postal service on a date. registered_ar delivery Proof the recipient received it. Use for terminations, formal notices, legal deadlines.
If the user asks for “a registered letter” for a legal matter (termination, formal notice, dispute),
registered_ar is almost always what they mean.
MCP server
Hosted, stateless, streamable HTTP at https://api.courio.si/mcp. No local install, no key. Add it as a
remote MCP server or custom connector:
{
"mcpServers": {
"courio": { "type": "http", "url": "https://api.courio.si/mcp" }
}
}
Tool Call it when Returns get_coverage Before quoting, to check a destination (country optional, ISO alpha-2). Services per country, “from” price, delivery days, legal product. quote_letter You have both addresses and the document. Free, commits to nothing. quote_id, fixed price (24 h), legal product, delivery estimate. send_letter The human agreed to the price. Same quote_id twice returns the same letter. letter_id, Stripe checkout_url for the human, next_step. get_letter_status To follow the letter after payment. Status, timeline, tracking number, proof links.
x402 payment is REST-only for now; MCP uses the Stripe link.
REST flow
1. Check coverage optional
curl "https://api.courio.si/v1/coverage?country=DE"
2. Quote
POST /v1/letters/quote. The document is exactly one of
{ "pdf_base64": "…" }, { "pdf_url": "https://…" } or
{ "text": "…", "format": "plain" | "markdown" }. PDF up to 15 MB and 20 pages. Addresses:
name (≤ 80), optional company, 1–3 address_lines,
postal_code, city, country (ISO alpha-2). Put the sender’s
email in sender. Optional options: color (default
false), duplex (default true); currency: EUR or USD.
No need to place the address in an envelope window: Courio adds an address page when needed (it is
counted in the price).
curl -X POST https://api.courio.si/v1/letters/quote \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-H "X-Agent-Id: my-agent/1.0" \
-d '{
"service": "registered_ar",
"sender": {
"name": "Jane Doe",
"address_lines": ["12 rue des Lilas"],
"postal_code": "75011", "city": "Paris", "country": "FR",
"email": "jane@example.com"
},
"recipient": {
"name": "Service résiliation",
"company": "Assur'Plus",
"address_lines": ["4 quai Perrache"],
"postal_code": "69002", "city": "Lyon", "country": "FR"
},
"document": { "text": "Madame, Monsieur,\n\nJe résilie...", "format": "markdown" }
}'
Response 200. Amounts are in cents, VAT included:
{
"quote_id": "qt_8fK2mQ0vXb7LwR4nT1yZc9Ae",
"expires_at": "2026-11-03T10:12:00Z",
"price": { "amount": 1290, "currency": "EUR", "tax_included": true, "usd_amount": 1405 },
"legal_product": {
"code": "FR_REGISTERED_AR",
"local_name": "Lettre recommandée avec AR",
"english_name": "Registered letter with proof of delivery",
"proof": "delivery"
},
"estimated_delivery_days": { "min": 2, "max": 4 },
"fulfillment": "automated",
"pages": 2,
"warnings": []
}
fulfillment: "manual" means no automated printer serves that route; a person mails it, with
the longer delay shown. Show price, legal_product.local_name and the delivery
estimate to the human now.
3. Create the letter and get paid (human in the loop)
curl -X POST https://api.courio.si/v1/letters \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"quote_id": "qt_8fK2mQ0vXb7LwR4nT1yZc9Ae",
"payment_method": "stripe",
"webhook_url": "https://agent.example.com/hooks/courio"
}'
201 Created
{
"letter_id": "ltr_3Hq9ZxV2pLm7Ns0Kd4Rt8Wy1",
"status": "awaiting_payment",
"payment": {
"checkout_url": "https://checkout.stripe.com/c/pay/cs_...",
"expires_at": "2026-11-03T10:14:00Z"
},
"webhook_secret": "9c1e…(64 hex chars)",
...
}
Give checkout_url to the human. The link expires after about 24 h; unpaid letters become
expired. Store webhook_secret if you passed a webhook_url: it is
returned only once.
Paying with x402 (autonomous agents)
If you can sign crypto payments, pay the quote yourself in USDC on Base using
x402. The amount is the quote’s usd_amount.
# 1. Ask for the letter with x402: no payment header yet
POST /v1/letters { "quote_id": "qt_...", "payment_method": "x402" }
-> 402 payment_required
PAYMENT-REQUIRED: <base64 requirements> (same object in the body: accepts[])
# 2. Sign a payment for those requirements (USDC on Base) with an x402 client,
# then resend the SAME request with the SAME Idempotency-Key
POST /v1/letters { "quote_id": "qt_...", "payment_method": "x402" }
PAYMENT-SIGNATURE: <base64 signed payment>
-> 201 { "letter_id": "ltr_...", "status": "submitted" | "paid", ... }
PAYMENT-RESPONSE: <base64 settlement>
- Once the payment settles, the response is never an error: you get
201 with submitted, or paid if submission continues in the background. - Official x402 clients cap a payment at $1 by default. Raise the spend limit to cover the quote first.
- x402 can be disabled on a deployment:
422 payment_method_unavailable means use stripe.
Tracking and webhooks
curl https://api.courio.si/v1/letters/ltr_3Hq9ZxV2pLm7Ns0Kd4Rt8Wy1
No auth: the letter_id is the credential. Without the API key that created it, the recipient
is reduced to city and country. Proof links (deposit for registered, delivery for
registered_ar) are signed and valid 15 minutes; fetch the letter again for fresh ones.
awaiting_payment → paid → submitted → in_transit →
delivered, or returned, failed (refunded), expired
(never paid), manual_pending → manual_done.
With a webhook_url (public https only), every status change is
POSTed as { letter_id, status, occurred_at, data }, retried with backoff for
24 h. Verify the Courio-Signature header:
import { createHmac, timingSafeEqual } from 'node:crypto';
// header: Courio-Signature: t=1762077120,v1=5f2c...
function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)));
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ''));
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return ok && fresh;
}
Errors
RFC 9457 application/problem+json, always with code and hint. A 5xx
or a network failure is safe to retry with the same Idempotency-Key.
codeHTTP What to do validation_failed 400 Fix the fields listed in `errors`. idempotency_key_required 400 Add an `Idempotency-Key` header (any unique string, e.g. a UUID). idempotency_key_reused 422 Same key, different body. Use a new key for a new request. payment_required 402 x402 only: pay the `accepts` requirements, resend with `PAYMENT-SIGNATURE`. payment_invalid 402 x402 payment refused; `reason` says why, fresh requirements attached. quote_already_used 409 This quote already has a letter: follow `letter_id`. quote_expired 410 Quotes last 24 h. Request a new quote. invalid_address 422 `field` names the address field to fix. product_unavailable 422 Service not offered for that country; pick the suggested level. document_invalid 422 PDF/text unusable; `reason` says why. content_rejected 422 Moderation refused the content. Do not retry with the same letter. amount_limit_exceeded 422 Price above the per-letter cap. payment_method_unavailable 422 x402 is off on this deployment: use `stripe`. limit_exceeded 429 Daily cap reached; retry after `retry_after` seconds, same key. moderation_unavailable 503 Nothing was accepted; retry the same request. payment_unavailable 503 Stripe or the facilitator is down; retry the same request.
Defaults: one letter per request, price capped per letter, a daily number of letters per IP, sender email,
wallet and API key. The complete list is in openapi.json.
Coverage
Printed in the destination country for these 10 countries. Every other ISO country
accepts standard only, sometimes handled manually (fulfillment: "manual", longer delay).
Human-readable list: /countries. Live data with prices:
GET /v1/coverage.
Country standardtrackedregisteredregistered_ar Austria AT Brief 1–3 days not offered Einschreiben 1–3 days Einschreiben mit Rückschein 1–3 days Belgium BE Lettre non prioritaire 2–3 days not offered Envoi recommandé 1–2 days Recommandé avec accusé de réception 1–2 days France FR Lettre verte 3–4 days Lettre suivie 3–4 days Lettre recommandée 2–4 days Lettre recommandée avec AR 2–4 days Germany DE Standardbrief 1–3 days Einschreiben Einwurf 1–3 days Einschreiben 1–3 days Einschreiben Rückschein 1–3 days Luxembourg LU Lettre 1–2 days not offered Envoi recommandé 1–2 days Recommandé avec avis de réception 1–2 days Netherlands NL Brief 1–2 days not offered Aangetekende brief 1–2 days Aangetekend met ontvangstbevestiging 1–2 days Spain ES Carta ordinaria 3–5 days not offered Carta certificada 2–4 days Carta certificada con aviso de recibo 2–4 days Switzerland CH B-Post Brief 2–3 days A-Post Plus 1–2 days Einschreiben 1–2 days Einschreiben mit Rückschein 1–2 days United Kingdom GB Second Class 2–3 days Tracked 48 2–3 days Signed For 1–3 days not offered United States US First-Class Mail 3–5 days First-Class Mail with tracking 3–5 days Certified Mail 3–5 days Certified Mail with Return Receipt 3–5 days