Posílej dopisy z vlastního systému
Jeden HTTP požadavek s textem a adresou a do schránky přistane vytištěný, ofrankovaný dopis. Bez tiskárny, bez známek, bez cesty na poštu.
Vytvořit klíčCelá integrace
To je všechno. Odpověď obsahuje id, kterým si vyžádáš stav, a totéž id se objeví na řádku tvé faktury.
curl -X POST https://sendletter.eu/api/v1/letters \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"text": "Geachte heer, mevrouw,\n\nHierbij zeg ik mijn abonnement op.",
"subject": "Opzegging",
"sender": { "name": "Jan de Vries", "street": "Merelstraat", "number": "64",
"postalCode": "8916 AX", "city": "Leeuwarden", "country": "NL" },
"recipient": { "company": "Voorbeeld BV", "name": "Afdeling Klantenservice",
"street": "Hoofdstraat", "number": "1",
"postalCode": "1011 AA", "city": "Amsterdam", "country": "NL" },
"product": "standard",
"idempotencyKey": "opzegging-2026-07-28"
}'const res = await fetch("https://sendletter.eu/api/v1/letters", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SENDLETTER_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ text, sender, recipient, product: "registered",
// Optional. Bills a company you belong to instead of
// this key's own wallet; leave it out for a personal
// letter. One you may not spend is refused, not billed
// to you.
organizationId: company.id,
idempotencyKey: invoice.id }),
})
const letter = await res.json()
// letter.id is what you store; poll it or wait for the webhook.
// letter.billedToOrganizationId says which wallet actually paid.$ch = curl_init("https://sendletter.eu/api/v1/letters");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("SENDLETTER_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"text" => $body, "sender" => $sender, "recipient" => $recipient,
"idempotencyKey" => $invoiceId,
]),
]);
$letter = json_decode(curl_exec($ch), true);import os, requests
letter = requests.post(
"https://sendletter.eu/api/v1/letters",
headers={"Authorization": f"Bearer {os.environ['SENDLETTER_KEY']}"},
json={"text": body, "sender": sender, "recipient": recipient,
"idempotencyKey": invoice_id},
timeout=30,
).json()Klíče
Každý požadavek nese API klíč jako bearer token. Klíče vytváříš ve svém účtu a každý z nich uvidíš přesně jednou; my ukládáme jen otisk. Zneplatnění klíče platí okamžitě.
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxVyzkoušej to dřív, než to odejde
Klíč začínající na sk_test_ ocení a vysází dopis přesně jako ostrý klíč, jen se zastaví těsně před tiskárnou. Můžeš tak protočit celou integraci, aniž by cokoli šlo na poštu.
Koncové body
| POST | /api/v1/letters | Send a letter |
| GET | /api/v1/letters | List letters, newest first |
| GET | /api/v1/letters/{id} | One letter, with its timeline |
| POST | /api/quote | Price, and what that country can be sent(no key) |
| GET | /api/countries | Destinations and their address rules(no key) |
| GET | /api/v1/openapi.json | The machine readable spec(no key) |
Ne každá cílová země má každý produkt. Zeptej se /api/quote a přečti availableProducts: do Nizozemska například doporučeně neexistuje. Můžeš tak možnost vypnout, místo aby selhala až při odesílání.
41 destinations. Registered mail: DE, BE, FR, ES, CH.
Statuses
- queued
- Accepted and waiting on the print partner.
- processing
- An exclusive hand-off to the print partner is in progress.
- attention_required
- The provider result is uncertain; held for reconciliation and never retried automatically.
- submitted
- Handed over to the printer.
- printed
- Printed and enveloped.
- posted
- Handed to the carrier. A response deadline runs from here.
- delivered
- Confirmed delivered. Registered mail only.
- failed
- Not sent. `statusDetail` says why, and the payment is returned.
Oznámení o stavu do tvých systémů
Dopis změní stav několikrát za několik dní. Dotazování se na takový rytmus nehodí: buď se ptáš mnohem častěji, než je třeba, nebo se to dozvíš mnohem později. Zaregistruj si adresu ve svém účtu a my ti dáme vědět.
POST your-endpoint
X-SendLetter-Event: letter.posted
X-SendLetter-Signature: 9f86d081...
{ "type": "letter.posted",
"sentAt": "2026-07-28T18:30:00.000Z",
"letter": { "id": "AbC123", "status": "posted", "trackingCode": null, ... } }Každé oznámení je podepsané. Spočítej HMAC-SHA256 ze surového těla svým tajným klíčem a porovnej ho s hlavičkou X-SendLetter-Signature. Udělej to dřív, než tělo naparsuješ: parsování a opětovná serializace mění bajty a podpis už nikdy nebude sedět.
import crypto from "node:crypto"
// The raw body, before any JSON parsing.
const expected = crypto.createHmac("sha256", process.env.WEBHOOK_SECRET)
.update(rawBody, "utf8").digest("hex")
const sent = req.headers["x-sendletter-signature"]
if (expected.length !== sent.length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sent))) {
return res.status(401).end()
}Chyby
Každá chyba má stejný tvar: kód, podle kterého se větví logika, a zprávu ke čtení. Větvi se podle kódu, protože zpráva se může změnit.
{ "error": { "code": "no_route",
"message": "Registered mail has no route to NL",
"details": null },
"requestId": "req_018f38d7c9f34738adbe1f06a9b36f11" }Every response also carries X-Request-Id. Include that value when asking support about one call; use the letter id to reconcile the complete order.
| 401 | unauthorised | No key, a revoked key, or a malformed header. |
| 400 | invalid_request | The body failed validation. `details` names the fields. |
| 402 | insufficient_balance | The wallet is short. `details` carries required and available. |
| 403 | insufficient_scope / credential_spending_limit | The key lacks a scope or its per-letter ceiling is too low. Nothing is charged. |
| 429 | rate_limited / credential_spending_limit | A request, daily-spend or monthly-spend ceiling was reached. |
| 403 | organization_forbidden | The named organisation exists and this account is in it, but its role may not spend there. Nothing is charged, to it or to you. |
| 404 | organization_not_found | No organisation of that id that this account belongs to. Refused rather than billed to your own wallet. |
| 409 | no_route | That product is not sold to that country. Ask /api/quote first. |
| 413 | too_many_pages | Over the sheet limit for that destination. |
| 404 | not_found | No such letter on this account. |
OpenAPI
Úplnou specifikaci servírujeme živě, takže nikdy nemůže popisovat verzi, která neběží. Nasměruj na ni generátor a máš klienta ve vlastním jazyce.