Weiter

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 anlegen

Mit 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
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"
  }'
Node.js
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.
PHP
$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);
Python
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_xxxxxxxxxxxxxxxxxxxx

Erst 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/lettersSend a letter
GET/api/v1/lettersList letters, newest first
GET/api/v1/letters/{id}One letter, with its timeline
POST/api/quotePrice, and what that country can be sent(no key)
GET/api/countriesDestinations and their address rules(no key)
GET/api/v1/openapi.jsonThe 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.

Node.js
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 } }
401unauthorisedNo key, a revoked key, or a malformed header.
400invalid_requestThe body failed validation. `details` names the fields.
402insufficient_balanceThe wallet is short. `details` carries required and available.
409no_routeThat product is not sold to that country. Ask /api/quote first.
413too_many_pagesOver the sheet limit for that destination.
404not_foundNo 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.