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:
- Lägg till din domän. Det är avsändardomänen — du får bara skicka från adresser på den.
- 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.
- 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
| Typ | Namn | Vä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.
| Typ | Namn | Vä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.
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
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ält | Typ | Beskrivning |
|---|---|---|
recipients | string[] | Krävs. En eller flera mottagaradresser. |
template_name | string | Krävs. Namnet på mallen som ska renderas. |
sender | string | Avsändaradress. Domändelen måste vara nyckelns domän. Krävs bara om domänen saknar standardavsändare. |
subject | string | Ämnesrad. Krävs bara om mallen saknar ämnesrad. |
sender_name | string | Visningsnamn för avsändaren. Faller tillbaka på domänens standardvärde. |
reply_to | string | Svarsadress, om den skiljer sig från avsändaren. Faller tillbaka på domänens standardvärde. |
cc | string[] | Kopia. |
bcc | string[] | Hemlig kopia. |
data | object | Vä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ält | Hämtas från |
|---|---|
sender | Domänens standardavsändare |
sender_name | Domänens standardnamn |
reply_to | Domänens standardsvarsadress |
subject | Mallens ä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.
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.
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ärde | Räknas som |
|---|---|
fältet saknas i data | falskt |
null | falskt |
"" | falskt |
0 | falskt |
false | falskt |
[] och {} | falskt |
| allt annat | sant |
Ska villkoret jämföra värden skrivs jämförelsen som ett funktionsanrop, med
funktionen först: eq .a .b, inte .a == .b.
| Skrivs | Betyder |
|---|---|
eq .a .b | lika. Fler argument betyder “lika med något av dem”: eq .status "ny" "väntar" |
ne .a .b | olika |
lt, le, gt, ge | mindre än, mindre eller lika, större än, större eller lika |
and .a .b, or .a .b, not .a | och, eller, inte |
len .lista | antal element i en lista, eller tecken i en sträng |
index .lista 0 | ett element ur en lista |
printf "%.2f" .belopp | formaterar 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 }}.
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.
.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.
{{ 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å:
- stream —
all,marketingellertransactional. 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 detall. - reason —
manual(standard),unsubscribe,complaintellerhard_bounce. Övriga orsaker sätter systemet självt.
Listan, nyast först, som { page, page_size, total_elements, data }.
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" }
}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.
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.
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”.
Hela listan som CSV.
En enskild post.
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": "…" }.
| Kod | Betyder |
|---|---|
400 | Nå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. |
401 | X-Api-Key saknas eller är okänd. |
404 | Domänens DKIM-nyckel är inte aktiv än — DNS är inte klart. Eller: posten finns inte. |
500 | Fel hos oss — eller ett fel i mallen, se Mallar. Inget skickades. |
Vanligast i början:
invalid sender domain—senderligger 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 required—template_namesaknas. Det fältet har inget standardvärde att falla tillbaka på.sender field is required if no default sender is set on domain— sättsenderi anropet, eller en standardavsändare på domänen.subject field is required if no subject is set on template— sättsubjecti 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.