Continuar

Envia cartas a partir do teu próprio sistema

Um pedido HTTP com texto e uma morada, e uma carta impressa e com os portes pagos aterra num capacho. Sem impressora, sem selos, sem ida ao marco do correio.

Criar uma chave

A integração completa

É isto tudo. A resposta traz um id com que lês o estado, e esse mesmo id aparece na linha da tua fatura.

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

Chaves

Cada pedido leva uma chave API como token bearer. Crias as chaves na tua conta e vês cada uma exatamente uma vez; nós guardamos apenas um hash. Revogar uma chave produz efeito de imediato.

Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxx

Testa antes de enviar

Uma chave começada por sk_test_ calcula o preço e compõe a carta tal e qual como uma chave de produção, e depois pára mesmo antes da impressora. Isso deixa-te exercitar a integração toda sem que nada saia para o correio.

Endpoints

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)

Nem todos os destinos vendem todos os produtos. Pergunta ao /api/quote e lê availableProducts: para os Países Baixos, por exemplo, não existe serviço registado. Assim podes desativar uma opção em vez de a deixares falhar na hora do envio.

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.

Avisos de estado para os teus sistemas

Uma carta muda de estado meia dúzia de vezes ao longo de vários dias. O polling não tem a forma certa para isso: ou perguntas vezes de mais, ou ficas a saber tarde de mais. Regista um endpoint na tua conta e nós avisamos-te.

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

Todos os avisos vão assinados. Calcula o HMAC-SHA256 sobre o corpo em bruto com o teu segredo e compara-o com o cabeçalho X-SendLetter-Signature. Fá-lo antes de interpretares o corpo: interpretar e voltar a serializar muda os bytes, e a assinatura nunca mais bate certo.

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

Erros

Todos os erros têm a mesma forma: um código para ramificar e uma mensagem para ler. Ramifica pelo código, porque a mensagem pode mudar.

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

A especificação completa é servida em direto, por isso nunca pode descrever uma versão que não está a correr. Aponta-lhe um gerador e ficas com um cliente na tua própria linguagem.