Documentation
One REST API over HTTPS, JSON in and JSON out. Everything below is what the API actually does today; nothing here is planned or partial.
Getting started
Create an API key under API Keys in the dashboard, then send. You can send from a sandbox key straight away; a real domain needs its DNS records first.
curl https://api.maillog.nl/v1/emails \
-H "Authorization: Bearer ma_your_key" \
-H "Content-Type: application/json" \
-d '{
"from": "Maillog <[email protected]>",
"to": ["[email protected]"],
"subject": "hello world",
"html": "<p>it works</p>"
}'The answer carries the id of the message. Use it on GET /v1/emails/:id to see how delivery went.
Authentication
Every call carries Authorization: Bearer <key>. A key belongs to one team, and you see the full key exactly once: at creation. After that we only keep a hash, so a lost key is replaced, never recovered.
| Role | May do |
|---|---|
| send_only | Send mail. Cannot read what was sent. |
| full_access | Everything this API offers, for this team. |
A sandbox key is a throwaway: mail sent with it never leaves the server and lands in a web inbox of its own. Handy for testing without touching a real domain.
Sending mail
Either html or text is required; sending both is fine and preferred. Everything else is optional.
| Field | Type | What it does |
|---|---|---|
| from | string | Sender, plain address or Name <[email protected]>. Must be on a domain you proved. |
| to | string[] | At least one recipient. |
| cc, bcc | string[] | Optional extra recipients. |
| reply_to | string | Where replies should go if that is not the sender. |
| subject | string | Required, at most 998 characters. |
| html, text | string | The body. At least one of the two. |
| headers | object | Extra headers, as key-value pairs. |
| tags | array | Labels of your own, as {name, value}. |
| attachments | array | filename plus base64 content. Inline images need a content_id. |
| scheduled_at | string | ISO timestamp. Held until then. |
| tracking | object | {opens, clicks}. May be stricter than the domain setting, never wider. |
Batch takes an array of the same objects and answers per message, so one rejected address does not sink the rest of the batch.
Domain setup
A domain has three layers that are switched on separately. Ownership first; without it the other two stay closed.
| Layer | Record | Why |
|---|---|---|
| Ownership | TXT | Proves the domain is yours. Required. |
| Sending | SPF + DKIM | Lets receiving servers see the mail really came from you. |
| Receiving | MX | Points incoming mail at us. Replaces your current mail server. |
| DMARC | TXT | Blocks nothing on its own, but tells the world what to do with forgeries. |
| Link domain | CNAME | Only if you measure clicks. Without it, links point at our address instead of yours. |
DNS spreads slowly. POST /v1/domains/:id/verify checks right now instead of waiting for the next scheduled run.
Webhooks
We POST JSON to your HTTPS URL when something happens to a message. The secret you receive at creation signs every call, so you can tell our calls from anyone else's.
email.sentemail.deliveredemail.openedemail.clickedemail.bouncedemail.complainedemail.failed// Node: check the signature before you trust the body
import crypto from "node:crypto";
const expected = crypto
.createHmac("sha256", process.env.MAILLOG_WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).end();
}Compare with timingSafeEqual and not with ===: a normal comparison stops at the first wrong character, and the time it took gives away how far someone guessed correctly.
Errors
Failures answer with a JSON body carrying statusCode and either message or a list of errors per field.
| Code | Means | What to do |
|---|---|---|
| 401 | Key missing or unknown | Check the Authorization header. |
| 403 | Key may not do this | A send_only key cannot read. Use a full_access key. |
| 404 | Not found for this team | Also what you get for something that belongs to someone else. |
| 422 | Body does not validate | Read errors: it names the field and the reason. |
| 429 | Too many requests | Back off and retry later. |
| 5xx | Our side broke | Retry; the same message is not sent twice. |
API reference
Every endpoint the API answers on today, grouped by what it is for.
Sending
One call sends one email. The batch call takes up to a hundred at a time and answers per message, so one bad address does not sink the rest.
| Method | Path | What it does |
|---|---|---|
| POST | /v1/emails | Send one email |
| POST | /v1/emails/batch | Send up to 100 at once |
| GET | /v1/emails | List sent emails, newest first |
| GET | /v1/emails/:id | One email with its delivery status |
Domains
A domain sits above sending and receiving. Prove ownership first; sending and receiving are separate switches after that.
| Method | Path | What it does |
|---|---|---|
| POST | /v1/domains | Add a domain |
| GET | /v1/domains | List domains with their DNS records |
| GET | /v1/domains/:id | One domain |
| PATCH | /v1/domains/:id | Change tracking or link domain |
| POST | /v1/domains/:id/verify | Check the DNS records now |
| DELETE | /v1/domains/:id | Remove a domain |
| GET | /v1/diagnostics/:domain | Read SPF, DKIM, DMARC and MX as they resolve right now |
API keys
A key is shown once, at creation. After that we only keep a hash, so a lost key is replaced, never recovered.
| Method | Path | What it does |
|---|---|---|
| POST | /v1/api-keys | Create a key |
| GET | /v1/api-keys | List keys (prefix only) |
| DELETE | /v1/api-keys/:id | Revoke a key immediately |
Webhooks
We POST an event to your URL. Only HTTPS, and every call carries an HMAC signature made with the secret you get at creation.
| Method | Path | What it does |
|---|---|---|
| POST | /v1/webhooks | Create a webhook and get its secret |
| GET | /v1/webhooks | List webhooks |
| PATCH | /v1/webhooks/:id | Change URL, events or enabled |
| DELETE | /v1/webhooks/:id | Delete a webhook |
Suppressions
Addresses we refuse to send to. Bounces and complaints land here on their own; you can add and remove by hand.
| Method | Path | What it does |
|---|---|---|
| GET | /v1/suppressions | List, optionally filtered with ?q= |
| POST | /v1/suppressions | Add an address by hand |
| DELETE | /v1/suppressions/:id | Allow mail to this address again |
Inbound
Mailboxes are the buckets, addresses are the name plates on the door, and routes decide what happens to mail that arrives.
| Method | Path | What it does |
|---|---|---|
| GET | /v1/inbound/mailboxes | List mailboxes |
| POST | /v1/inbound/mailboxes | Create a mailbox |
| PATCH | /v1/inbound/mailboxes/:id | Change storage, quota or retention |
| POST | /v1/inbound/mailboxes/:id/addresses | Add an address to a mailbox |
| DELETE | /v1/inbound/mailboxes/:id | Delete an empty mailbox |
| GET | /v1/inbound/routes | List routing rules |
| POST | /v1/inbound/routes | Create a routing rule |
| GET | /v1/inbound/messages | List received messages |
| GET | /v1/inbound/messages/:id/body | Message body, sanitised |
| GET | /v1/inbound/messages/:id/attachments/:index | One attachment |
| GET | /v1/inbound/logs | What happened to each arriving message |
Contacts, segments and broadcasts
Contacts belong to your team, segments are filters over them, and a broadcast is one mail to a segment.
| Method | Path | What it does |
|---|---|---|
| GET | /v1/contacts | List contacts |
| POST | /v1/contacts | Create a contact |
| PATCH | /v1/contacts/:id | Update a contact |
| DELETE | /v1/contacts/:id | Delete a contact |
| GET | /v1/segments | List segments |
| POST | /v1/segments | Create a segment |
| GET | /v1/topics | List subscription topics |
| POST | /v1/broadcasts | Create a broadcast |
| POST | /v1/broadcasts/:id/send | Send a broadcast |
| GET | /v1/templates | List templates |
| POST | /v1/templates | Create a template |
| POST | /v1/templates/:id/publish | Publish a template version |
Secure portal
Instead of the message, the recipient gets a link. The body stays encrypted with us and is handed over only against a token, and optionally a PIN.
| Method | Path | What it does |
|---|---|---|
| POST | /v1/secure | Store a message and get its link |
| POST | /v1/secure/retrieve | Retrieve a message with token and PIN |
| GET | /v1/secure/status/:id | Whether it was opened, and when |
Account and monitoring
Your team, your workspaces, and what the API did.
| Method | Path | What it does |
|---|---|---|
| GET | /v1/metrics | Daily numbers, ?days=7|30|90 |
| GET | /v1/logs | Every API call, newest first |
| GET | /v1/team/members | Team members and their roles |
| POST | /v1/team/invites | Invite someone |
| GET | /v1/workspaces | List workspaces |
| POST | /v1/workspaces/:id/switch | Switch the active workspace |
| GET | /v1/addons | Paid add-ons and their state |