Continuer

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

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_xxxxxxxxxxxxxxxxxxxx

Essayer 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/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)

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.

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

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

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.