Dokumentation

Postministeriets api är ett enda anrop för att skicka e-post, plus en spärrlista som håller reda på vilka adresser du inte får mejla. Allt är JSON över HTTPS.

Kom igång

Tre steg i portalen innan första anropet:

  1. Lägg till din domän. Det är avsändardomänen — du får bara skicka från adresser på den.
  2. Sätt upp DNS. Portalen visar de fyra poster domänen behöver: en SPF-post, en DKIM-post och två poster för retur-adressen. Alla fyra måste gå att slå upp innan domänen aktiveras — vi kontrollerar dem varje minut och aktiverar domänen automatiskt när de svarar.
  3. Skapa en api-nyckel. Nyckeln hör till en domän. Den visas en gång när den skapas, så spara den direkt.

SPF och DKIM

TypNamnVärde
TXT @ v=spf1 include:_spf.postministeriet.se -all
TXT <selektor>._domainkey v=DKIM1; k=rsa; p=… (unik per domän, hämtas i portalen)

Skickar du redan e-post från domänen via någon annan: slå ihop till en SPF-post — flera SPF-poster på samma namn gör att uppslaget misslyckas helt.

Retur-adress på din egen domän

Studsar, avvisningar och klagomål kommer tillbaka till en retur-adress, och din ligger på pm-bounce.dindomän.se. Den behöver två egna poster, och de krävs precis som SPF och DKIM för att domänen ska aktiveras.

TypNamnVärde
MX pm-bounce 10 mx.postministeriet.se
TXT pm-bounce v=spf1 include:_spf.postministeriet.se -all

MX-posten är vägen tillbaka för rapporterna. TXT-posten är den mottagaren kontrollerar spf mot, eftersom spf kontrolleras mot retur-adressens domän och inte mot den avsändare som syns i mejlet. Har din dns-leverantör ett eget fält för prioritet skriver du 10 där och bara mx.postministeriet.se som värde.

Vinsten är två saker: spf hamnar på din egen domän i stället för vår, vilket ger dig ett andra godkänt underlag för dmarc vid sidan av dkim, och ryktet som retur-adressen bygger upp hos mottagarna blir ditt eget i stället för delat med alla andra som skickar via oss.

Båda posterna behövs Var för sig gör de ingen nytta: en MX utan spf tar emot post som mottagaren hade rätt att avvisa, och en spf-post utan MX pekar ut ett namn ingenting kan levereras till. Därför räknas de som en enda punkt i portalen — antingen svarar båda, eller så är retur-adressen inte på plats.

Vi flyttar retur-adressen till din domän så snart båda posterna svarar. Fram till dess ligger den kvar på vår, så inget slutar fungera medan du väntar på att dns sprids — men domänen aktiveras inte förrän posterna är på plats.

Autentisering

Bas-url är https://api.postministeriet.se. Varje anrop autentiseras med api-nyckeln i headern X-Api-Key:

X-Api-Key: DIN_API_NYCKEL

Nyckeln är domänen. Därför nämns aldrig någon domän i anropen — en nyckel når exakt den domän den skapades för, och ingen annan.

Saknad eller okänd nyckel ger 401 Unauthorized.

Skicka e-post

POST /api/message

Ett utskick görs alltid mot en mall som ligger på domänen. Mallen skapar du i portalen; anropet pekar ut den med namn och skickar med de värden den behöver.

Fält

FältTypBeskrivning
recipientsstring[]Krävs. En eller flera mottagaradresser.
template_namestringKrävs. Namnet på mallen som ska renderas.
senderstringAvsändaradress. Domändelen måste vara nyckelns domän. Krävs bara om domänen saknar standardavsändare.
subjectstringÄmnesrad. Krävs bara om mallen saknar ämnesrad.
sender_namestringVisningsnamn för avsändaren. Faller tillbaka på domänens standardvärde.
reply_tostringSvarsadress, om den skiljer sig från avsändaren. Faller tillbaka på domänens standardvärde.
ccstring[]Kopia.
bccstring[]Hemlig kopia.
dataobjectVärdena mallen fyller i.

Standardvärden

Fyra av fälten behöver du inte skicka varje gång. Utelämnar du dem hämtas de därifrån de hör hemma:

FältHämtas från
senderDomänens standardavsändare
sender_nameDomänens standardnamn
reply_toDomänens standardsvarsadress
subjectMallens ämnesrad

Ett värde i anropet vinner alltid över standardvärdet. De tre första sätter du per domän i portalen — standardavsändaren måste ligga på domänen, precis som en avsändare i anropet. Ämnesraden sätter du på mallen.

Saknas både fältet och standardvärdet avvisas anropet med 400: sender field is required if no default sender is set on domain respektive subject field is required if no subject is set on template.

Layoutens ämnesrad används inte Ligger mallen i en layout är det mallens egen ämnesrad som gäller. En ämnesrad satt på layouten läses aldrig.

Exempel

curl -X POST https://api.postministeriet.se/api/message \
  -H "X-Api-Key: DIN_API_NYCKEL" \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": ["mottagare@example.com"],
    "sender": "no-reply@dindoman.se",
    "sender_name": "Din Domän",
    "subject": "Välkommen till Postministeriet",
    "template_name": "valkommen",
    "data": { "namn": "Anna" }
  }'

Har domänen en standardavsändare och mallen en ämnesrad räcker det med mottagare, mall och data:

curl -X POST https://api.postministeriet.se/api/message \
  -H "X-Api-Key: DIN_API_NYCKEL" \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": ["mottagare@example.com"],
    "template_name": "valkommen",
    "data": { "namn": "Anna" }
  }'

Svar

{
  "id": "0f6c2f1e-4a3b-4c7d-9e11-2b8a5d6f7c40",
  "pending_count": 1,
  "suppressed_count": 0
}

200 OK betyder att meddelandet är skapat och köat, inte att det redan är levererat. Utskicket sker strax efteråt av en separat process, och meddelanden som av någon anledning inte gått iväg plockas upp och försöks igen.

pending_count är hur många kuvert som faktiskt går ut. suppressed_count är hur många mottagare som hoppades över för att de står på spärrlistan — de finns då också uppräknade i fältet suppressed. Ett utskick till tjugo mottagare där en har avregistrerat sig går alltså ut till nitton och talar om varför, i stället för att avvisas.

Rå html/text utan mall Endpointen POST /api/message/raw finns i routingen men är ännu inte färdig — den validerar anropet och svarar 200 utan att skicka något. Använd POST /api/message tills vidare.

Mallar

En mall har en html-del och en textdel, och kan ligga i en layout som är gemensam för flera mallar. Platshållarna skrivs {{ .fältnamn }} och fylls från data-objektet i anropet:

<h1>Hej {{ .namn }}!</h1>
<p>Din order {{ .ordernummer }} är på väg.</p>
"data": { "namn": "Anna", "ordernummer": "10424" }

Skiftläge spelar ingen roll: {{ .namn }}, {{ .Namn }} och {{ .NAMN }} läser samma värde. Värden html-escapas automatiskt i html-delen.

Villkor: if, else if, else

En mall kan välja text efter vad som ligger i data. Villkoret inleds med {{ if … }}, kan följas av valfritt antal {{ else if … }} och ett {{ else }}, och avslutas alltid med {{ end }}:

{{ if eq .status "betald" }}
  <p>Tack! Vi packar din order nu.</p>
{{ else if eq .status "väntar" }}
  <p>Vi väntar fortfarande på din betalning.</p>
{{ else }}
  <p>Din order är avbruten.</p>
{{ end }}

Står det bara ett fältnamn i villkoret — {{ if .rabattkod }} — är frågan om fältet har något innehåll. Det här räknas som falskt:

VärdeRäknas som
fältet saknas i datafalskt
nullfalskt
""falskt
0falskt
falsefalskt
[] och {}falskt
allt annatsant

Ska villkoret jämföra värden skrivs jämförelsen som ett funktionsanrop, med funktionen först: eq .a .b, inte .a == .b.

SkrivsBetyder
eq .a .blika. Fler argument betyder “lika med något av dem”: eq .status "ny" "väntar"
ne .a .bolika
lt, le, gt, gemindre än, mindre eller lika, större än, större eller lika
and .a .b, or .a .b, not .aoch, eller, inte
len .listaantal element i en lista, eller tecken i en sträng
index .lista 0ett element ur en lista
printf "%.2f" .beloppformaterar ett värde

Parenteser grupperar, så ett sammansatt villkor blir {{ if and .prenumerant (not .uppsagd) }} och ett räknat blir {{ if gt (len .rader) 3 }}.

Jämför tal med decimalpunkt Tal ur data är alltid decimaltal, eftersom de kommer ur json. Jämför därför med ett decimaltal: {{ if gt .belopp 100.0 }}. Skriver du 100 misslyckas renderingen med incompatible types for comparison: float64 and int och inget skickas.

Värden som kan saknas

Ett fält som inte finns i data stoppar inte utskicket. I html-delen blir det tomt — men i textdelen skrivs det ut som <no value>, mitt i mejlet. Det är den vanligaste orsaken till skräptext i textversionen, och skälet att alltid vakta fält som inte är med varje gång.

Tre sätt, beroende på vad som ska hända när värdet saknas:

Hej {{ or .namn "kund" }}!

{{ if .rabattkod }}Använd koden {{ .rabattkod }} i kassan.{{ end }}

{{ with .adress }}
  Levereras till {{ .gata }}, {{ .postort }}.
{{ else }}
  Vi återkommer om leveransadressen.
{{ end }}
  • or .namn "kund" ger ett reservvärde: första värdet som inte är tomt vinner.
  • {{ if .rabattkod }} tar bort hela stycket när fältet saknas.
  • {{ with .adress }} gör två saker: hoppar över stycket när fältet saknas, och gör . till värdet inuti, så fälten under det läses direkt som {{ .gata }}. Ett {{ else }} körs när det saknas.
Tomt är ofarligt, fel form är det inte Att läsa ett fält som saknas går bra. Att läsa ett fält ur något som inte är ett objekt gör det inte: är .kund en sträng misslyckas {{ .kund.namn }}. Samma sak med {{ len .rader }} när rader saknas helt — vakta den med {{ if .rader }} först.

Loopar

{{ range … }} upprepar ett stycke en gång per element i en lista. Inuti loopen är . det aktuella elementet, så fälten på elementet läses som {{ .artikel }}:

<table>
{{ range .rader }}
  <tr><td>{{ .artikel }}</td><td>{{ .antal }} st</td><td>{{ .pris }} kr</td></tr>
{{ end }}
</table>
"data": {
  "rader": [
    { "artikel": "Kaffe",  "antal": 2, "pris": 89 },
    { "artikel": "Filter", "antal": 1, "pris": 29 }
  ]
}

Är listan en enkel lista av strängar eller tal skriver du ut elementet självt med {{ . }}:

{{ range .taggar }}<span>{{ . }}</span> {{ end }}

Ett {{ else }} i en range körs när listan är tom eller fältet saknas — du behöver alltså inget separat {{ if }} runt omkring:

{{ range .rader }}
  <p>{{ .artikel }}</p>
{{ else }}
  <p>Din order är tom.</p>
{{ end }}

Behöver du numret på raden, eller elementet under ett eget namn, deklarerar du dem i loopen. Numret börjar på 0:

{{ range $nr, $rad := .rader }}
  <p>{{ $nr }}. {{ $rad.artikel }} — {{ $rad.pris }} kr</p>
{{ end }}

Eftersom . pekar på elementet inuti loopen når du fälten på toppnivån med $. i stället. Villkor och loopar kan ligga i varandra hur djupt som helst:

{{ range .ordrar }}
  <h2>Order {{ .nummer }} till {{ $.kundnamn }}</h2>
  {{ range .rader }}
    <p>{{ .artikel }}{{ if gt .antal 1.0 }} ({{ .antal }} st){{ end }}</p>
  {{ end }}
{{ end }}

Pekar range på ett objekt i stället för en lista körs den en gång per värde i objektet, i bokstavsordning på nycklarna.

Loopa bara över listor {{ range … }} kräver en lista eller ett objekt. Pekar den på en sträng eller ett tal misslyckas renderingen med range can't iterate over …. Ett fält som saknas är däremot ofarligt — det behandlas som en tom lista och kör {{ else }}.

Radbrytningar runt villkor och loopar

{{ if }}, {{ range }} och {{ end }} skriver inte ut något själva, men raden de står på finns kvar i resultatet. I html syns det inte, i textdelen blir det tomrader. Ett bindestreck innanför klammern äter blanksteg och radbrytning på den sidan:

Din beställning:
{{- range .rader }}
- {{ .artikel }}
{{- end }}

När renderingen misslyckas

En mall som inte går att tolka eller köra stoppar hela anropet: det svarar 500 med felet från mallen i message, och ingenting skickas till någon mottagare. Testutskicket i portalen är stället att fånga det på, eftersom det kör exakt samma rendering med data du väljer själv.

En mall kan också ha en ämnesrad. Den används när anropet inte anger någon — se Standardvärden.

En layout innehåller {{ .Content }} där mallens innehåll ska in. En layout kan inte skickas som mall i sig — anropet avvisas då med 400. Layoutens egen ämnesrad används aldrig.

Spärrlista

Spärrlistan är adresserna domänen inte får mejla: studsar, klagomål och avregistreringar. Den kontrolleras både när meddelandet skapas och strax innan det går ut, så en avregistrering som kommer in emellan hinner räknas.

Varje post har två egenskaper värda att förstå:

  • streamall, marketing eller transactional. En avregistrering från ett nyhetsbrev stoppar marknadsföring, inte lösenordsåterställningen personen själv begär tio minuter senare. Utelämnas fältet blir det all.
  • reasonmanual (standard), unsubscribe, complaint eller hard_bounce. Övriga orsaker sätter systemet självt.
GET /api/suppressions?page=1&page_size=25&include_removed=false

Listan, nyast först, som { page, page_size, total_elements, data }.

GET /api/suppressions/check?address=…&stream=all

Svarar på frågan “får jag mejla den här adressen”, utan att skicka något:

{
  "address": "spärrad@example.com",
  "stream": "all",
  "suppressed": true,
  "entry": { "reason": "unsubscribe", "created": "2026-08-14T09:12:03Z" }
}
POST /api/suppression
curl -X POST https://api.postministeriet.se/api/suppression \
  -H "X-Api-Key: DIN_API_NYCKEL" \
  -H "Content-Type: application/json" \
  -d '{
    "address": "kund@example.com",
    "stream": "marketing",
    "reason": "unsubscribe",
    "note": "Avregistrerad via vår egen sida"
  }'

Att lägga till en adress som redan står på listan returnerar posten som finns i stället för att misslyckas — samma anrop två gånger lämnar dig på samma ställe.

POST /api/suppressions/bulk

Import, upp till 10 000 poster per anrop. stream, reason och note på toppnivå gäller hela importen och kan skrivas över per post.

{
  "reason": "unsubscribe",
  "stream": "marketing",
  "entries": [
    { "address": "en@example.com" },
    { "address": "tva@example.com", "reason": "hard_bounce", "stream": "all" }
  ]
}

Svaret talar om vad importen gjorde:

{ "submitted": 2, "added": 2, "skipped": 0 }

skipped är adresser som redan stod på listan — de skrivs inte över, posten som redan finns är den som säger varför. Adresser som inte går att tolka stoppar inte importen, de kommer tillbaka i invalid.

GET /api/suppressions/history?address=…

Alla poster som någonsin skrivits för en adress, även upphävda. Det är hit man går för att svara på “varför slutade jag få er e-post”.

GET /api/suppressions/export?include_removed=false

Hela listan som CSV.

GET /api/suppression/{id}

En enskild post.

DELETE /api/suppression/{id}

Upphäver spärren, vilket återaktiverar e-post till adressen. Svarar 204 No Content. Posten raderas inte utan stämplas med när den upphävdes — spåret är hela poängen med listan.

Felkoder

Fel svarar med JSON: { "message": "…" }.

KodBetyder
400Något i anropet är fel: ogiltig adress, saknat fält som varken anropet eller standardvärdena fyller i, avsändardomän som inte hör till nyckeln, okänd mall, eller kvoten är slut.
401X-Api-Key saknas eller är okänd.
404Domänens DKIM-nyckel är inte aktiv än — DNS är inte klart. Eller: posten finns inte.
500Fel hos oss — eller ett fel i mallen, se Mallar. Inget skickades.

Vanligast i början:

  • invalid sender domainsender ligger inte på den domän nyckeln skapades för.
  • domain key not active — DNS-posterna svarar inte än. Kontrollera domänens status i portalen.
  • template name is requiredtemplate_name saknas. Det fältet har inget standardvärde att falla tillbaka på.
  • sender field is required if no default sender is set on domain — sätt sender i anropet, eller en standardavsändare på domänen.
  • subject field is required if no subject is set on template — sätt subject i anropet, eller en ämnesrad på mallen.

Kvot

Varje konto har en månadskvot. När den är slut svarar utskicksanropet 400 med monthly quota exceeded, och räknaren nollställs den första i månaden. Mottagare som hoppas över för att de står på spärrlistan räknas inte.