API-dokumentation

Alle endpoints ligger under https://postly.dk/api/public/v1 og svarer med JSON.

Autentificering

Send din nøgle som bearer token. Nøglen vises kun én gang ved oprettelsen.

Authorization: Bearer pk_postly_xxxxxxxxxxxxxxxx

Konto og saldo

GET /api/public/v1/account

{
  "id": "...",
  "name": "Eksempel ApS",
  "balanceOre": 250000,
  "status": "active"
}

Priser

GET /api/public/v1/pricing

Send et brev

Enten text eller file (base64 af en PDF). Beløbet trækkes fra saldoen. Send gerne en Idempotency-Key, så gentagne kald ikke sender to breve. I får en kvittering på mail med det samme og en mail, når brevet er sendt.

POST /api/public/v1/letters
Idempotency-Key: ordre-1234

{
  "letterType": "almindelig",
  "sender": {
    "name": "Eksempel ApS",
    "address": "Nordre Fasanvej 207A",
    "zip": "2000",
    "city": "Frederiksberg"
  },
  "recipient": {
    "name": "Jens Jensen",
    "address": "Vestergade 1",
    "zip": "8000",
    "city": "Aarhus"
  },
  "text": "Kære Jens, her følger brevet.",
  "reference": "faktura-1234"
}

Afsendelsesbekræftelse

Når vi har sendt brevet, får I en mail med ordrenummer og afsendelsesdato.

Testnøgler

Opret en testnøgle i /konto/api. Den starter med pk_test_postly_. Et brev sendt med en testnøgle bliver aldrig printet eller sendt, og saldoen bliver ikke trukket. Efter ca. 2 minutter bliver det afleveret til dao med en indleveringskode, der starter med TEST, og et rekommanderet testbrev får sit udfald 3 minutter senere. Med feltet testOutcome vælger I udfaldet: delivered (standard), attempted eller not_collected. Ved delivered kommer en eksempel-PDF som kvittering. Breve fra testnøgler har "livemode": false.

curl https://postly.dk/api/public/v1/letters \
  -H "Authorization: Bearer pk_test_postly_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: paakrav-lejemaal-12-2026-11" \
  -d '{
    "letterType": "rekommanderet",
    "reference": "Lejemål 12",
    "testOutcome": "delivered",
    "sender": { "name": "Ejendomsadministrationen", "address": "Vej 1", "zip": "8000", "city": "Aarhus C" },
    "recipient": { "name": "Lejer Hansen", "address": "Vej 2, 1. th", "zip": "8000", "city": "Aarhus C" },
    "text": "Påkrav ..."
  }'

Status, reference og kvittering

Hvert brev har jeres reference, en samlet deliveryStatus (processing, handed_to_carrier, og for rekommanderede breve delivered, attempted eller not_collected), dao's indleveringskode i trackingNumber, og om kvitteringen er klar. Det gælder både enkeltbreve og breve i en udsendelse.

GET /api/public/v1/letters?reference=Lejemål%2012
GET /api/public/v1/letters/{id}
GET /api/public/v1/letters/{id}/receipt      # dao's kvittering, link i 10 minutter
GET /api/public/v1/letters/{id}/declaration  # Postlys afsendelseserklæring

Webhooks

Opret en webhook i /konto/api eller via API'et. Vi sender en POST, når et brev er afleveret til dao (letter.handed_to_carrier), når dao melder et udfald (letter.delivery_outcome), og når kvitteringen er klar (letter.receipt_available). Svar med 2xx. Ellers prøver vi igen efter 1 min, 5 min, 30 min, 2 t, 12 t og 24 t. Hemmeligheden vises kun, når webhooken oprettes.

POST /api/public/v1/webhooks          { "url": "https://jeres-system.dk/postly" }
GET  /api/public/v1/webhooks
POST /api/public/v1/webhooks/{id}/test   # sender en signeret test.ping
DELETE /api/public/v1/webhooks/{id}

{
  "id": "…",
  "event": "letter.delivery_outcome",
  "createdAt": "2026-11-06T10:15:00.000Z",
  "livemode": true,
  "data": { "id": "…", "reference": "Lejemål 12", "deliveryStatus": "delivered", … }
}

Tjek signaturen i headeren Postly-Signature (t=<unix>,v1=<hex>): beregn HMAC-SHA256 af t + "." + body med hemmeligheden, sammenlign i konstant tid, og afvis beskeder, der er mere end 5 minutter gamle. Brug den rå body, før den parses.

// Node.js
import crypto from "node:crypto";

function verifyPostly(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return (
    expected.length === parts.v1.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
  );
}
# Python
import hmac, hashlib, time

def verify_postly(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts["t"])
    if abs(time.time() - t) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Faktura i stedet for saldo

Efter aftale kan kontoen faktureres månedligt bagud i stedet for at bruge saldo. Så får I aldrig 402, og den 1. i måneden kommer én faktura med alle breve fra måneden før (dato, reference, brevtype, sider og pris) og 8 dages betalingsfrist. Fakturaen ligger i /konto/saldo. GET /account viser faktureringsform og aftalte priser.

Hele API'et er beskrevet i openapi.json. Spørgsmål: gustav@postly.dk.

Fyld saldo op

Returnerer et betalingslink. Saldoen opdateres, når betalingen er gennemført.

POST /api/public/v1/topups

{ "amountOre": 100000 }

Fejl

Fejl returneres som { "error": { "code": "...", "message": "..." } } med en passende HTTP-status. Ved for lav saldo får du 402 og koden insufficient_balance.

Grænser

120 kald i timen pr. nøgle. Filer kan maks. fylde 25 MB.

MCP-server

Forbind en AI-assistent til Postly med samme nøgle. Værktøjerne er priser, saldo, send brev, brevstatus og oversigt over breve.

URL:  https://postly.dk/api/public/v1/mcp
Auth: Authorization: Bearer pk_postly_xxxxxxxx