Envoyez des courriers depuis votre propre système
Une requête HTTP avec un texte et une adresse, et une lettre imprimée et affranchie arrive dans la boîte. Sans imprimante, sans timbres, sans passer à la poste.
Créer une cléEn un seul appel
C'est toute l'intégration. La réponse contient un identifiant qui sert à suivre le statut, et ce même identifiant figure sur votre ligne de facture.
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()Clés
Chaque requête porte une clé API en jeton bearer. Vous créez vos clés dans votre compte et chacune ne s'affiche qu'une fois; nous n'en gardons qu'une empreinte. Une révocation prend effet immédiatement.
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxEssayer avant de poster
Une clé commençant par sk_test_ tarifie et compose la lettre exactement comme une clé réelle, puis s'arrête juste avant l'imprimante. Vous éprouvez ainsi toute l'intégration sans que rien ne parte.
Points d'entrée
| 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) |
Tous les pays ne vendent pas tous les produits. Interrogez /api/quote et lisez availableProducts: le recommandé vers les Pays-Bas n'existe pas, par exemple. Vous pouvez alors désactiver une option au lieu de la laisser échouer à l'envoi.
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.
Notifications vers vos systèmes
Une lettre change de statut quelques fois seulement, étalées sur plusieurs jours. L'interrogation répétée convient mal: soit vous demandez bien trop souvent, soit vous l'apprenez bien trop tard. Déclarez un point d'entrée dans votre compte et nous vous prévenons.
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, ... } }Chaque notification est signée. Calculez un HMAC-SHA256 sur le corps brut avec votre secret et comparez-le à l'en-tête X-SendLetter-Signature. Faites-le avant d'analyser le corps: l'analyser puis le resérialiser change les octets, et la signature ne correspondra plus jamais.
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()
}Erreurs
Toute erreur a la même forme: un code sur lequel brancher et un message à lire. Branchez sur le code, car le message peut changer.
{ "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
La spécification complète est servie en direct: elle ne peut donc jamais décrire une version qui ne tourne pas. Pointez-y un générateur et vous obtenez un client dans votre langage.