Utskicks-API
Skicka orderbekräftelser, kvitton och formulärsvar från era egna system. Mejlen signeras med er domän och går ut från samma servrar som er vanliga e-post hos Alweo.
Kom igång
- Domänen ska ha e-post hos Alweo, med SPF, DKIM och DMARC på plats. Hälsokollen i panelen visar om allt stämmer.
- En administratör skapar en nyckel i Alweo-panelen under E-post → Utskicks-API. Nyckeln är knuten till en domän och visas bara en gång, så spara den direkt.
- Lägg nyckeln i ert system, helst som en miljövariabel och aldrig i koden.
- Skicka ett första mejl med exemplet nedan.
Bas-adressen är https://alweo.se/api/v1. Allt skickas och tas emot som JSON i UTF-8.
Hela API:t finns också som OpenAPI-fil (3.1) som kan läsas in i Postman, Insomnia eller en kodgenerator.
Autentisering
Skicka nyckeln i rubriken Authorization:
Authorization: Bearer alw_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Nycklar börjar med alw_. Vi sparar bara ett kontrollvärde av nyckeln, inte själva nyckeln. Den kan alltså inte visas igen. En borttappad nyckel tar ni bort i panelen och skapar en ny.
Testnycklar
När ni bygger och provar kopplingen kan ni använda en testnyckel. Den skapar ni på samma ställe i panelen genom att kryssa i Testnyckel. Testnycklar börjar med alw_test_.
- Anropen kontrolleras precis som vanligt, med samma fält, gränser och felkoder.
- Inga mejl skickas. Varje mottagare markeras direkt som
deliveredmed förklaringen att inget skickades. - Webhooks skickas som vanligt, med
"test": trueidata. - Svaret innehåller
"test": true, och testutskick räknas inte mot månadstaket.
{ "id": "6f1c…", "test": true }
Byt till en vanlig nyckel när kopplingen fungerar.
Skicka ett mejl
| Fält | Typ | Beskrivning |
|---|---|---|
from krävs | sträng | Avsändare, antingen order@ertforetag.se eller Webbshoppen <order@ertforetag.se>. Adressen måste ligga på nyckelns domän. |
to krävs | sträng eller lista | En eller flera mottagare. |
cc valfri | sträng eller lista | Kopia. |
bcc valfri | sträng eller lista | Dold kopia. |
subject krävs | sträng | Ämnesrad, högst 900 tecken. |
html html eller text | sträng | Mejlet som HTML. |
text html eller text | sträng | Mejlet som vanlig text. Skicka gärna både html och text, det ger bättre leverans. |
reply_to valfri | sträng | Adress som svar ska gå till. |
headers valfri | objekt | Egna rubriker, till exempel {"X-Order": "1234"}. |
attachments valfri | lista | Bilagor, se Bilagor. |
Högst 50 mottagare totalt i to, cc och bcc. Ska samma mejl till många, skicka ett anrop per mottagare så att ingen ser någon annans adress.
curl https://alweo.se/api/v1/emails \
-H "Authorization: Bearer $ALWEO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "Webbshoppen <order@ertforetag.se>",
"to": ["kund@example.com"],
"subject": "Tack för din order",
"html": "<p>Vi har tagit emot din order.</p>",
"text": "Vi har tagit emot din order."
}'const svar = await fetch("https://alweo.se/api/v1/emails", {
method: "POST",
headers: {
"Authorization": "Bearer " + process.env.ALWEO_API_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
from: "Webbshoppen <order@ertforetag.se>",
to: ["kund@example.com"],
subject: "Tack för din order",
html: "<p>Vi har tagit emot din order.</p>",
text: "Vi har tagit emot din order."
})
});
const data = await svar.json();
if (!svar.ok) throw new Error(data.name + ": " + data.message);
console.log(data.id);$ch = curl_init("https://alweo.se/api/v1/emails");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("ALWEO_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"from" => "Webbshoppen <order@ertforetag.se>",
"to" => ["kund@example.com"],
"subject" => "Tack för din order",
"html" => "<p>Vi har tagit emot din order.</p>",
"text" => "Vi har tagit emot din order.",
]),
]);
$data = json_decode(curl_exec($ch), true);
$kod = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($kod !== 200) throw new Exception($data["name"] . ": " . $data["message"]);
echo $data["id"];import os, requests
svar = requests.post(
"https://alweo.se/api/v1/emails",
headers={"Authorization": "Bearer " + os.environ["ALWEO_API_KEY"]},
json={
"from": "Webbshoppen <order@ertforetag.se>",
"to": ["kund@example.com"],
"subject": "Tack för din order",
"html": "<p>Vi har tagit emot din order.</p>",
"text": "Vi har tagit emot din order.",
},
timeout=60,
)
data = svar.json()
if not svar.ok:
raise RuntimeError(f'{data["name"]}: {data["message"]}')
print(data["id"])Svar
{ "id": "6f1c2a7e-3b4d-4f5a-9c8e-1d2b3c4d5e6f" }
Spara id om ni vill följa leveransen. Om någon mottagare fanns på spärrlistan togs den bort ur utskicket, och svaret visar vilka:
{ "id": "6f1c…", "suppressed": ["gammal@example.com"] }
Svaret 200 betyder att vår mejlserver har tagit emot mejlet för leverans. Om det sedan kommer fram ser ni under Hämta status eller via webhooks.
Bilagor
Varje bilaga är ett objekt med filens innehåll kodat i base64:
| Fält | Beskrivning |
|---|---|
filename | Filnamnet mottagaren ser, till exempel faktura-1234.pdf. |
content | Filens innehåll i base64. |
content_type valfri | Till exempel application/pdf. |
"attachments": [
{ "filename": "faktura-1234.pdf", "content": "JVBERi0xLjQK…", "content_type": "application/pdf" }
]
Högst 20 bilagor och 10 MB sammanlagt per mejl, räknat på filernas storlek före base64-kodningen. Större filer skickar ni hellre som en länk.
Hämta status
Visar ett mejl som skickats med samma nyckel, och hur leveransen gått för varje mottagare.
{
"id": "6f1c…",
"from": "order@ertforetag.se",
"recipients": 1,
"subject": "Tack för din order",
"status": "sent",
"error": null,
"created_at": "2026-10-09T05:41:12.000Z",
"delivery": {
"kund@example.com": {
"status": "delivered",
"detail": "250 2.0.0 OK …",
"at": "2026-10-09T05:41:14.000Z"
}
}
}
| Värde | Betyder |
|---|---|
status: "sent" | Vår server tog emot mejlet för leverans. |
status: "failed" | Mejlet kunde inte skickas, orsaken står i error. |
delivered | Mottagarens server tog emot mejlet. |
delivery_delayed | Mottagarens server svarade med ett tillfälligt fel. Vi försöker igen automatiskt. |
bounced | Mejlet kunde inte levereras. detail visar svaret från mottagarens server. |
Leveransstatus uppdateras ungefär varannan minut, så delivery är tomt direkt efter utskicket. Status följs i tre dygn. Ibland tar mottagarens server först emot mejlet och skickar en studs senare. Den studsen kommer som ett vanligt mejl till avsändaradressen och syns inte här.
Webhooks
Med en webhook skickar vi en händelse till er server så fort något händer, i stället för att ni frågar efter status. En administratör lägger till adressen i panelen under E-post → Utskicks-API → Webhooks. Adressen måste börja med https://, och ett företag kan ha högst fem webhooks. När webhooken skapas visas en hemlighet som börjar med whsec_. Den visas bara en gång.
Händelser
| Typ | Skickas när | Innehåll i data |
|---|---|---|
email.sent | Vår server har tagit emot mejlet. | email_id, from, to (lista), subject |
email.delivered | Mottagarens server tog emot mejlet. | email_id, to, detail |
email.delivery_delayed | Tillfälligt fel, vi försöker igen. | email_id, to, detail |
email.bounced | Mejlet kunde inte levereras. | email_id, to, detail |
email.test | Ni trycker på Skicka testhändelse i panelen. | message, webhook_id |
email.suppressed | Adressen har lagts på spärrlistan efter en permanent studs. | email_id, to, reason |
Varje händelse är en POST med JSON. För mejl med flera mottagare kommer en händelse per mottagare, utom email.sent.
POST https://er-server.se/alweo-webhook
Content-Type: application/json
User-Agent: Alweo-Webhooks/1
Alweo-Signature: t=1760000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
{
"type": "email.delivered",
"created_at": "2026-10-09T05:41:14.000Z",
"data": {
"email_id": "6f1c…",
"to": "kund@example.com",
"detail": "250 2.0.0 OK …"
}
}
Skicka en testhändelse
Vid varje webhook i panelen finns knappen Skicka testhändelse. Den skickar direkt en signerad händelse av typen email.test och visar vad er server svarade och hur lång tid det tog. Då vet ni att adressen nås och kan kontrollera att signaturen godkänns.
{ "type": "email.test", "created_at": "…", "data": { "message": "Testhändelse från Alweo. Svara med 2xx.", "webhook_id": "12" } }
Svara och nya försök
Svara med en 2xx-kod inom 10 sekunder. Ett annat svar, eller inget svar alls, räknas som misslyckat. Vi försöker då igen efter 1, 5, 15, 60 och 180 minuter, alltså högst sex försök. Senaste svaret syns vid webhooken i panelen.
Samma händelse kan i sällsynta fall komma två gånger, och ordningen är inte garanterad. Använd email_id, to och type för att känna igen en händelse ni redan tagit hand om.
Kontrollera signaturen
Rubriken Alweo-Signature visar att händelsen kommer från oss och inte har ändrats på vägen. Den har formen t=<tid>,v1=<signatur>, där:
tär tiden då händelsen skickades, i sekunder sedan 1970 (Unix-tid).v1är HMAC-SHA256 i hex, med webhookens hemlighet som nyckel, av textent + "." + rå kropp.
Räkna fram samma värde och jämför. Använd den råa kroppen precis som den kom, inte JSON som tolkats och kodats om. Avvisa gärna händelser där t är mer än fem minuter gammal.
const crypto = require("crypto");
const express = require("express");
const app = express();
app.post("/alweo-webhook", express.raw({ type: "application/json" }), (req, res) => {
const rubrik = req.get("Alweo-Signature") || "";
const t = (/t=(\d+)/.exec(rubrik) || [])[1];
const v1 = (/v1=([0-9a-f]{64})/.exec(rubrik) || [])[1];
if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400);
const vantat = crypto.createHmac("sha256", process.env.ALWEO_WEBHOOK_SECRET)
.update(t + "." + req.body.toString("utf8")).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(vantat), Buffer.from(v1))) return res.sendStatus(401);
const handelse = JSON.parse(req.body);
console.log(handelse.type, handelse.data);
res.sendStatus(200);
});$kropp = file_get_contents("php://input");
$rubrik = $_SERVER["HTTP_ALWEO_SIGNATURE"] ?? "";
preg_match('/t=(\d+)/', $rubrik, $t);
preg_match('/v1=([0-9a-f]{64})/', $rubrik, $v1);
if (!$t || !$v1 || abs(time() - (int) $t[1]) > 300) { http_response_code(400); exit; }
$vantat = hash_hmac("sha256", $t[1] . "." . $kropp, getenv("ALWEO_WEBHOOK_SECRET"));
if (!hash_equals($vantat, $v1[1])) { http_response_code(401); exit; }
$handelse = json_decode($kropp, true);
// $handelse["type"], $handelse["data"]
http_response_code(200);import hmac, hashlib, os, re, time
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/alweo-webhook")
def alweo_webhook():
kropp = request.get_data()
rubrik = request.headers.get("Alweo-Signature", "")
t = re.search(r"t=(\d+)", rubrik)
v1 = re.search(r"v1=([0-9a-f]{64})", rubrik)
if not t or not v1 or abs(time.time() - int(t.group(1))) > 300:
abort(400)
vantat = hmac.new(os.environ["ALWEO_WEBHOOK_SECRET"].encode(),
t.group(1).encode() + b"." + kropp, hashlib.sha256).hexdigest()
if not hmac.compare_digest(vantat, v1.group(1)):
abort(401)
handelse = request.get_json()
print(handelse["type"], handelse["data"])
return "", 200Spärrlistan
När ett mejl studsar permanent, till exempel för att adressen inte finns, läggs adressen på ert företags spärrlista. Det skyddar er domäns rykte, eftersom upprepade utskick till döda adresser får andra mejlservrar att misstro avsändaren.
- Spärrade adresser tas bort ur kommande utskick och redovisas i svarets
suppressed. - Är alla mottagare spärrade svarar API:t med
422 recipients_suppressed. - Tillfälliga fel, som en full brevlåda eller en server som inte svarar, spärrar inte adressen.
- Har mottagaren rättat sin adress tar en administratör bort den från spärrlistan i panelen.
Gränser och nivåer
| Gräns | Värde |
|---|---|
| Mottagare per mejl | 1–50, räknat på to, cc och bcc tillsammans |
| Bilagor | Högst 20 stycken och 10 MB sammanlagt |
| Hela anropet | Högst 15 MB JSON |
| Per nyckel och dygn | 1 000 mejl, räknat bakåt 24 timmar från anropet |
| Per månad och företag | Beror på nivån och räknas i mottagare per kalendermånad (svensk tid) |
| Nivå | Mottagare per månad | Pris |
|---|---|---|
| Gratis | 10 | 0 kr i en månad. Provet börjar när första nyckeln skapas. |
| Start | 10 000 | 1 788 kr/år exkl. moms |
| Pro | 50 000 | 3 588 kr/år exkl. moms |
Nivån väljs i panelen under E-post → Utskicks-API. Behöver ni mer än dygnstaket eller Pro, mejla hello@alweo.se. Vi går igenom utskicken innan gränsen höjs.
Felkoder
Ett fel svarar alltid med JSON i samma form:
{ "statusCode": 403, "name": "invalid_from", "message": "Nyckeln får bara skicka från @ertforetag.se" }
| Kod | name | Betyder |
|---|---|---|
| 401 | unauthorized | Nyckel saknas, är felskriven eller har tagits bort. |
| 402 | trial_ended | Provmånaden är slut. Välj Start eller Pro i panelen. |
| 403 | invalid_from | Avsändaren ligger inte på nyckelns domän. |
| 404 | not_found | Mejlet finns inte, eller skickades med en annan nyckel. |
| 422 | validation_error | Något fält saknas eller är fel. message säger vilket. |
| 422 | recipients_suppressed | Alla mottagare finns på spärrlistan. |
| 429 | rate_limit_exceeded | Nyckelns dygnstak är nått. |
| 429 | monthly_quota_exceeded | Månadstaket för nivån är nått. |
| 500 | internal_error | Något gick fel hos oss. Försök igen. |
| 502 | send_failed | Mejlservern kunde inte ta emot mejlet. Försök igen om en stund. |
| 503 | not_configured | Utskick är tillfälligt avstängt. |
Fel med kod 4xx försvinner inte av att ni försöker igen, utan anropet måste rättas. Vid 500, 502 och 503 kan ni försöka igen efter en stund, gärna med växande väntetid. Pågående störningar visas på driftstatus.
Tips för bra leverans
- Skicka både
htmlochtext. - Använd en avsändare som går att svara till, eller sätt
reply_to. - Skicka bara till personer som väntar sig mejlet, som kunder som lagt en order eller fyllt i ett formulär. Nyhetsbrev till stora listor sköts bäst med ett utskicksverktyg för nyhetsbrev.
- Ta bort adresser som studsar ur ert eget system. Lyssna på
email.bouncedochemail.suppressed. - Ha en nyckel per system, till exempel en för webbshoppen och en för affärssystemet. Då kan ni byta eller ta bort en utan att det påverkar den andra.
Frågor om API:t: hello@alweo.se