Fortsæt

Send breve fra dit eget system

Én HTTP-forespørgsel med tekst og en adresse, og så lander der et printet brev med porto på i en brevsprække. Ingen printer, ingen frimærker, ingen tur til postkassen.

Opret en nøgle

Hele integrationen

Det er det hele. Svaret indeholder et id, du bruger til at læse status, og det samme id står på din fakturalinje.

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",
                         // 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.
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()

Nøgler

Hver forespørgsel bærer en API-nøgle som bearer-token. Du opretter nøgler på din konto og ser hver enkelt præcis én gang; vi gemmer kun et hash. Trækker du en nøgle tilbage, holder den op med at virke med det samme.

Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxx

Prøv det af, før det går i posten

En nøgle, der begynder med sk_test_, prissætter og opsætter brevet nøjagtigt som en rigtig nøgle gør, men stopper lige før printeren. Sådan kan du køre hele integrationen igennem, uden at der går noget af sted.

Endepunkter

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)

Ikke alle modtagerlande sælger alle produkter. Spørg /api/quote og læs availableProducts: der findes for eksempel ikke anbefalet brev til Holland. Så kan du slå en mulighed fra i stedet for at lade den fejle ved afsendelsen.

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.

Statusbeskeder til dine egne systemer

Et brev skifter tilstand en håndfuld gange hen over nogle dage. Polling passer dårligt til den rytme: enten spørger du alt for tit, eller også hører du det alt for sent. Registrer et endepunkt på din konto, så siger vi til.

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, ... } }

Hver besked er signeret. Beregn HMAC-SHA256 over den rå body med din hemmelighed, og sammenlign resultatet med headeren X-SendLetter-Signature. Gør det, før du parser body: parsning og ny serialisering ændrer bytene, og så kommer signaturen aldrig til at passe igen.

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()
}

Fejl

Alle fejl har samme form: en kode at forgrene på og en besked at læse. Forgren på koden, for beskeden må gerne ændre sig.

{ "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.

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.
403insufficient_scope / credential_spending_limitThe key lacks a scope or its per-letter ceiling is too low. Nothing is charged.
429rate_limited / credential_spending_limitA request, daily-spend or monthly-spend ceiling was reached.
403organization_forbiddenThe named organisation exists and this account is in it, but its role may not spend there. Nothing is charged, to it or to you.
404organization_not_foundNo organisation of that id that this account belongs to. Refused rather than billed to your own wallet.
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

Hele specifikationen serveres live, så den kan aldrig beskrive en version, der ikke kører. Peg en generator på den, og du har en klient i dit eget sprog.