Briefe aus Ihrem eigenen System versenden
Eine HTTP-Anfrage mit Text und Anschrift, und ein gedruckter, frankierter Brief landet im Briefkasten. Kein Drucker, keine Marken, kein Gang zum Postamt.
Schlüssel anlegenMit einem Aufruf
Mehr ist es nicht. Die Antwort enthält eine ID, mit der Sie den Status abfragen, und dieselbe ID steht auf Ihrer Rechnungszeile.
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",
idempotencyKey: invoice.id }),
})
const letter = await res.json()
// letter.id is what you store; poll it or wait for the webhook.$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()Schlüssel
Jede Anfrage trägt einen API-Schlüssel als Bearer-Token. Schlüssel legen Sie in Ihrem Konto an und sehen jeden genau einmal; wir speichern nur einen Hash. Ein Widerruf wirkt sofort.
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxErst prüfen, dann verschicken
Ein Schlüssel, der mit sk_test_ beginnt, berechnet und rendert den Brief genau wie ein echter und hält kurz vor dem Drucker an. So testen Sie die ganze Anbindung, ohne dass etwas hinausgeht.
Endpunkte
| 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) |
Nicht jedes Land verkauft jedes Produkt. Fragen Sie /api/quote und lesen Sie availableProducts: ein Einschreiben in die Niederlande gibt es zum Beispiel nicht. So schalten Sie eine Option ab, statt sie beim Versand scheitern zu lassen.
15 destinations. Registered mail: DE, BE, FR, ES, CH.
Statuses
- queued
- Accepted and waiting on the print partner.
- 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.
Statusmeldungen an Ihre Systeme
Ein Brief wechselt über mehrere Tage nur ein paar Mal den Status. Pollen passt dazu nicht: Sie fragen viel zu oft oder erfahren es viel zu spät. Hinterlegen Sie einen Endpunkt in Ihrem Konto, wir melden uns.
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, ... } }Jede Meldung ist signiert. Bilden Sie HMAC-SHA256 über den unveränderten Body mit Ihrem Secret und vergleichen Sie das Ergebnis mit dem Header X-SendLetter-Signature. Tun Sie das, bevor Sie den Body parsen: Parsen und erneutes Serialisieren ändert die Bytes, und dann passt die Signatur nie mehr.
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()
}Fehler
Jeder Fehler hat dieselbe Form: ein Code zum Verzweigen und eine Nachricht zum Lesen. Verzweigen Sie über den Code, denn die Nachricht darf sich ändern.
{ "error": { "code": "no_route",
"message": "Registered mail has no route to NL",
"details": null } }| 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. |
| 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
Die vollständige Spezifikation wird live ausgeliefert und kann daher nie eine Version beschreiben, die nicht läuft. Richten Sie einen Generator darauf, und Sie haben einen Client in Ihrer Sprache.