ALWEO API
Documentation Läs på svenska

Sending API

Send order confirmations, receipts and form replies from your own systems. Emails are signed with your domain and sent from the same servers as your regular email at Alweo.

Getting started

  1. The domain must have email at Alweo, with SPF, DKIM and DMARC in place. The health check in the panel shows whether everything is correct.
  2. An administrator creates a key in the Alweo panel under Email → Sending API. The key is tied to one domain and is shown only once, so save it right away.
  3. Add the key to your system, preferably as an environment variable and never in the code.
  4. Send a first email using the example below.

The base URL is https://alweo.se/api/v1. All requests and responses are JSON in UTF-8.

The whole API is also available as an OpenAPI file (3.1) that you can import into Postman, Insomnia or a code generator.

Authentication

Send the key in the Authorization header:

Authorization: Bearer alw_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Keys start with alw_. We store only a check value of the key, not the key itself, so it cannot be shown again. If you lose a key, delete it in the panel and create a new one.

Only use the key from a server. Never put it in a web page or app where visitors can read it. Anyone with the key can send email from your domain.

Test keys

While you build and try out the integration, you can use a test key. Create it in the same place in the panel by ticking Test key. Test keys start with alw_test_.

{ "id": "6f1c…", "test": true }

Switch to a regular key once the integration works.

Send an email

POST/api/v1/emails
FieldTypeDescription
from requiredstringSender, either order@yourcompany.se or Web shop <order@yourcompany.se>. The address must be on the key's domain.
to requiredstring or listOne or more recipients.
cc optionalstring or listCarbon copy.
bcc optionalstring or listBlind carbon copy.
subject requiredstringSubject line, at most 900 characters.
html html or textstringThe email as HTML.
text html or textstringThe email as plain text. Sending both html and text is recommended; it improves deliverability.
reply_to optionalstringAddress that replies should go to.
headers optionalobjectCustom headers, for example {"X-Order": "1234"}.
attachments optionallistAttachments, see Attachments.

At most 50 recipients in total across to, cc and bcc. To send the same email to many people, make one request per recipient so that no one sees anyone else's address.

curl https://alweo.se/api/v1/emails \
  -H "Authorization: Bearer $ALWEO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Web shop <order@yourcompany.se>",
    "to": ["customer@example.com"],
    "subject": "Thanks for your order",
    "html": "<p>We have received your order.</p>",
    "text": "We have received your order."
  }'

Response

{ "id": "6f1c2a7e-3b4d-4f5a-9c8e-1d2b3c4d5e6f" }

Save the id if you want to track delivery. If any recipient was on the suppression list, it was removed from the send, and the response lists which ones:

{ "id": "6f1c…", "suppressed": ["old@example.com"] }

A 200 response means our mail server has accepted the email for delivery. Whether it then arrives is shown under Get status or via webhooks.

Attachments

Each attachment is an object with the file content encoded in base64:

FieldDescription
filenameThe file name the recipient sees, for example invoice-1234.pdf.
contentThe file content in base64.
content_type optionalFor example application/pdf.
"attachments": [
  { "filename": "invoice-1234.pdf", "content": "JVBERi0xLjQK…", "content_type": "application/pdf" }
]

At most 20 attachments and 10 MB in total per email, measured on the file sizes before base64 encoding. Larger files are better sent as a link.

Get status

GET/api/v1/emails/{id}

Returns an email sent with the same key, and the delivery result for each recipient.

{
  "id": "6f1c…",
  "from": "order@yourcompany.se",
  "recipients": 1,
  "subject": "Thanks for your order",
  "status": "sent",
  "error": null,
  "created_at": "2026-10-09T05:41:12.000Z",
  "delivery": {
    "customer@example.com": {
      "status": "delivered",
      "detail": "250 2.0.0 OK …",
      "at": "2026-10-09T05:41:14.000Z"
    }
  }
}
ValueMeaning
status: "sent"Our server accepted the email for delivery.
status: "failed"The email could not be sent; the reason is in error.
deliveredThe recipient's server accepted the email.
delivery_delayedThe recipient's server responded with a temporary error. We retry automatically.
bouncedThe email could not be delivered. detail shows the response from the recipient's server.

Delivery status is updated about every two minutes, so delivery is empty right after sending. Status is tracked for three days. Sometimes the recipient's server first accepts the email and sends a bounce later. That bounce arrives as a regular email to the sender address and is not shown here.

Webhooks

With a webhook, we send an event to your server as soon as something happens, instead of you polling for status. An administrator adds the URL in the panel under Email → Sending API → Webhooks. The URL must start with https://, and a company can have at most five webhooks. When the webhook is created, a secret starting with whsec_ is shown. It is shown only once.

Events

TypeSent whenContents of data
email.sentOur server has accepted the email.email_id, from, to (list), subject
email.deliveredThe recipient's server accepted the email.email_id, to, detail
email.delivery_delayedTemporary error; we are retrying.email_id, to, detail
email.bouncedThe email could not be delivered.email_id, to, detail
email.testYou click Send test event in the panel.message, webhook_id
email.suppressedThe address has been added to the suppression list after a permanent bounce.email_id, to, reason

Each event is a POST with JSON. For emails with several recipients, there is one event per recipient, except for email.sent.

POST https://your-server.com/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": "customer@example.com",
    "detail": "250 2.0.0 OK …"
  }
}

Send a test event

Each webhook in the panel has a Send test event button. It immediately sends a signed event of type email.test and shows what your server responded and how long it took. This confirms that the URL is reachable and lets you check that the signature is accepted.

{ "type": "email.test", "created_at": "…", "data": { "message": "Test event from Alweo. Respond with 2xx.", "webhook_id": "12" } }

Responses and retries

Respond with a 2xx code within 10 seconds. Any other response, or no response at all, counts as a failure. We then retry after 1, 5, 15, 60 and 180 minutes, so at most six attempts. The latest response is shown with the webhook in the panel.

In rare cases the same event can arrive twice, and the order is not guaranteed. Use email_id, to and type to recognize an event you have already handled.

Verify the signature

The Alweo-Signature header shows that the event comes from us and has not been modified in transit. It has the form t=<time>,v1=<signature>, where:

Compute the same value and compare. Use the raw body exactly as it arrived, not JSON that has been parsed and re-encoded. We recommend rejecting events where t is more than five minutes old.

const crypto = require("crypto");
const express = require("express");
const app = express();

app.post("/alweo-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("Alweo-Signature") || "";
  const t = (/t=(\d+)/.exec(header) || [])[1];
  const v1 = (/v1=([0-9a-f]{64})/.exec(header) || [])[1];
  if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400);

  const expected = crypto.createHmac("sha256", process.env.ALWEO_WEBHOOK_SECRET)
    .update(t + "." + req.body.toString("utf8")).digest("hex");
  if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) return res.sendStatus(401);

  const event = JSON.parse(req.body);
  console.log(event.type, event.data);
  res.sendStatus(200);
});

Suppression list

When an email bounces permanently, for example because the address does not exist, the address is added to your company's suppression list. This protects your domain's reputation, since repeated sends to dead addresses make other mail servers distrust the sender.

Limits and plans

LimitValue
Recipients per email1–50, counting to, cc and bcc together
AttachmentsAt most 20 files and 10 MB in total
Entire requestAt most 15 MB of JSON
Per key and day1,000 emails, counted over the 24 hours before the request
Per month and companyDepends on the plan, counted in recipients per calendar month (Swedish time)
PlanRecipients per monthPrice
Free100 SEK for one month. The trial starts when the first key is created.
Start10,0001,788 SEK/yr excl. VAT
Pro50,0003,588 SEK/yr excl. VAT

The plan is selected in the panel under Email → Sending API. If you need more than the daily limit or the Pro plan, email hello@alweo.se. We review your sending before the limit is raised.

Error codes

Errors always respond with JSON in the same form:

{ "statusCode": 403, "name": "invalid_from", "message": "The key may only send from @yourcompany.se" }
CodenameMeaning
401unauthorizedThe key is missing, misspelled or has been deleted.
402trial_endedThe trial month has ended. Choose Start or Pro in the panel.
403invalid_fromThe sender is not on the key's domain.
404not_foundThe email does not exist, or was sent with a different key.
422validation_errorA field is missing or invalid. message says which one.
422recipients_suppressedAll recipients are on the suppression list.
429rate_limit_exceededThe key's daily limit has been reached.
429monthly_quota_exceededThe monthly quota for the plan has been reached.
500internal_errorSomething went wrong on our side. Try again.
502send_failedThe mail server could not accept the email. Try again shortly.
503not_configuredSending is temporarily disabled.

4xx errors do not go away by retrying; the request must be corrected. On 500, 502 and 503 you can retry after a while, preferably with increasing wait times. Ongoing incidents are shown on the status page.

Tips for good deliverability

Questions about the API: hello@alweo.se