Courio
Get a quote

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…UsePayment
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

  1. 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.
  2. Over MCP you cannot pay. send_letter returns a Stripe link; give it to the human and ask them to open it.
  3. 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).
  4. 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.
  5. Read the hint. Every error is application/problem+json with a stable code and a one-sentence hint telling you what to do next.
  6. 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.

serviceProofUse 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" }
  }
}
ToolCall it whenReturns
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.

codeHTTPWhat 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