Mailumi API reference

Email API documentation.

The Mailumi email API is a JSON API over HTTPS at https://mailumi.com/api/v1. Authenticate with a Bearer API key, send email with POST /emails or POST /emails/batch, read messages and delivery events with GET /emails, and get delivery events and incoming email as signed webhooks.

Quickstart

  1. Create a free account and confirm your email address.
  2. Add your domain in Dashboard → Domains and add the DNS records shown. On Cloudflare, Mailumi can add them for you.
  3. Create an API key in Dashboard → API keys and store it in your server’s environment variables as MAILUMI_API_KEY.
  4. Send your first email with one HTTPS request from your backend, then follow it in Email activity.
curl -X POST https://mailumi.com/api/v1/emails \
  -H "Authorization: Bearer $MAILUMI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: welcome-user-123" \
  -d '{
    "from": "Acme <hello@yourdomain.com>",
    "to": "alex@example.com",
    "subject": "Welcome aboard",
    "html": "<p>Your account is ready.</p>",
    "text": "Your account is ready."
  }'

For Node.js, Bun, Deno and Cloudflare Workers, use the official mailumi package. Other languages need no SDK: any language that can make an HTTPS request can use Mailumi.

Node.js SDK and CLI

The official mailumi package is a typed client with no dependencies for Node.js 18+, Bun, Deno, Cloudflare Workers and any other runtime with fetch, as ESM or CommonJS. Every method resolves to { data, error } and never throws for API errors, the same shape as the Resend SDK, so switching from Resend means changing the import and the API key.

Shell
npm install mailumi
TypeScript
import { Mailumi } from 'mailumi';

const mailumi = new Mailumi(); // reads MAILUMI_API_KEY

// Send later, with an attachment.
const { data, error } = await mailumi.emails.send({
  from: 'Acme <billing@yourdomain.com>',
  to: ['alex@example.com'],
  replyTo: 'support@yourdomain.com',
  subject: 'Your invoice',
  text: 'Your invoice is attached.',
  attachments: [{ filename: 'invoice.pdf', content: pdfBytes }],
  scheduledAt: 'in 1 hour',
}, { idempotencyKey: `invoice/${invoice.id}` });

// Check delivery, or cancel while it is scheduled.
await mailumi.emails.get(data.id);
await mailumi.emails.cancel(data.id);

// Up to 100 emails in one request.
await mailumi.batch.send([
  { from, to: 'a@example.com', subject, html },
  { from, to: 'b@example.com', subject, html },
]);

// Contacts for campaigns (needs an API key with contacts:write).
await mailumi.contacts.create({ audienceId: 'aud_…', email: 'alex@example.com' });

Options accept camelCase (replyTo, scheduledAt, templateId) as well as the API’s snake_case. Attachment content can be bytes or a base64 string. Rate limits (429) and temporary errors (5xx) are retried twice with backoff. Each send carries an Idempotency-Key, reused on every retry, so a retry never sends twice; pass { idempotencyKey } to choose your own. verifyWebhook checks webhook signatures, as shown under Webhooks.

MethodEndpoint
emails.send(email, { idempotencyKey })POST /emails
batch.send(emails, { idempotencyKey })POST /emails/batch
emails.list({ direction, before })GET /emails
emails.get(id)GET /emails/{id}
emails.update({ id, scheduledAt })PATCH /emails/{id}
emails.cancel(id)POST /emails/{id}/cancel
emails.attachment({ id, index })GET /emails/{id}/attachments/{index}
domains.list()GET /domains
audiences.list(), create({ name }), get(id), update({ id, name }), remove(id)/audiences and /audiences/{id}
contacts.create(), import(), list(), get(), update(), remove()/audiences/{id}/contacts and /audiences/{id}/contacts/{contact}
verifyWebhook({ payload, headers, secret })Runs locally; needs no API key

The package also installs the mailumi command. Set MAILUMI_API_KEY, then send email, check delivery and manage contacts from a terminal or a CI job. Add --json for machine-readable output; errors exit with code 1.

Shell
npx mailumi send --from "Acme <hello@yourdomain.com>" --to alex@example.com \
  --subject "Deploy finished" --text "Version 2.4 is live." --attach ./report.pdf
npx mailumi emails get <id>
npx mailumi domains list
npx mailumi contacts add <audience_id> alex@example.com --first-name Alex
npx mailumi --help

Send with SMTP

Software that only speaks SMTP can send through Mailumi too: WordPress, Supabase Auth, Ghost, Django, Laravel, Rails, Nodemailer, PHPMailer, monitoring tools and printers. Use these settings:

SettingValue
Hostsmtp.mailumi.com
Port465 (TLS) or 587 (STARTTLS)
Usernamemailumi
PasswordAn API key with the email:send permission
EncryptionRequired: TLS 1.2 or newer before you log in
import nodemailer from 'nodemailer';

const transport = nodemailer.createTransport({
  host: 'smtp.mailumi.com',
  port: 465,
  secure: true,
  auth: { user: 'mailumi', pass: process.env.MAILUMI_API_KEY },
});

await transport.sendMail({
  from: 'Acme <hello@yourdomain.com>',
  to: 'alex@example.com',
  subject: 'Welcome to Acme',
  html: '<p>Thanks for signing up.</p>',
});

Email sent through SMTP is handled exactly like an API request: the From address must be on a verified domain, it counts toward your plan, suppressed recipients are skipped, and it appears in Email activity and sends the same webhook events. The success reply contains the email ID, as in 250 Queued as 3f2c…, which you can look up with GET /emails/{id}.

  • Recipients come from the SMTP envelope. Addresses in the To and Cc headers stay visible; every other envelope recipient is sent as Bcc.
  • Attachments and inline images (cid:) are kept, as are threading and list headers (In-Reply-To, References, List-Unsubscribe) and your own X- headers.
  • Up to 50 recipients per message and 20 MB per message, with attachments under 10 MB in total.
  • If your client resends a message after a dropped connection, the same Message-ID and recipients return the original email instead of sending it twice.
  • Use AUTH PLAIN or AUTH LOGIN. Any username works; mailumi is a good choice. Restrict the key to one domain if the app only sends from that domain.
ReplyMeaning
250Accepted and queued for delivery.
535Login failed: the API key is wrong, revoked, expired or lacks email:send.
550Rejected for good, with the reason: for example a From domain that is not verified, a recipient that is not a valid address, or the plan limit. Your client should not retry.
451Temporary, for example the rate limit of 120 requests per minute. Send the message again a little later; mail servers and apps with a mail queue do this on their own.
452More than 50 recipients; the rest are refused and can go in another message.
552The message is too large.
421Too many failed logins from your address; wait 15 minutes.

The SMTP server runs in Amsterdam on Fly.io. It does not store messages: each one is passed straight to the Mailumi API in the EU over HTTPS and handled from there. For new code, the HTTP API or the Node.js SDK gives you more, such as scheduling, templates, tags and batch sends.

Build with AI

Using Cursor, Claude Code, Codex, GitHub Copilot, Windsurf, Lovable, Bolt, v0 or Replit? Point your assistant at the complete reference in one file, llms-full.txt, or this page as Markdown, docs.md. Ready-made prompts for sending, webhooks, receiving and switching providers are on the Build with AI page.

To teach your assistant Mailumi for good, save these rules in your project as AGENTS.md, CLAUDE.md or a Cursor rule in .cursor/rules/mailumi.mdc:

AGENTS.md
# Mailumi email API

This project sends and receives email with Mailumi. Complete reference: https://mailumi.com/llms-full.txt

- Base URL: https://mailumi.com/api/v1. Authenticate with the header "Authorization: Bearer $MAILUMI_API_KEY". Call Mailumi only from server-side code; never expose the key to browsers or mobile apps.
- JavaScript and TypeScript: use the official package (npm install mailumi). const mailumi = new Mailumi(process.env.MAILUMI_API_KEY); const { data, error } = await mailumi.emails.send({ from, to, subject, html }, { idempotencyKey }). Methods never throw for API errors; they resolve to { data, error }. Verify webhooks with verifyWebhook({ payload: rawBody, headers, secret }).
- Send: POST /emails with JSON { from, to, subject, html and/or text } and optional cc, bcc, reply_to, attachments [{ filename, content (base64), content_type, content_id }], headers, tags [{ name, value }] and scheduled_at. The from domain must be verified in Mailumi.
- Batch: POST /emails/batch with an array of up to 100 emails (no attachments or scheduled_at). Templates: pass template_id and variables instead of subject and html.
- Software that only speaks SMTP: host smtp.mailumi.com, port 465 (TLS) or 587 (STARTTLS), username mailumi, password $MAILUMI_API_KEY.
- Always send an Idempotency-Key header that is stable for each logical email. Retry 429 and 503 with the same key.
- Errors are JSON { "error": "message", "code": "machine_readable_code" } with a 4xx or 5xx status. On 429, wait the seconds in the Retry-After header. OpenAPI description: https://mailumi.com/openapi.json
- Webhooks use the Standard Webhooks format. Verify webhook-signature ("v1," + base64 HMAC-SHA256 of webhook-id + "." + webhook-timestamp + "." + raw body) with the base64 part of MAILUMI_WEBHOOK_SECRET after "whsec_", in constant time. Reject timestamps older than 5 minutes and process each event id once.
- Events: email.sent, email.delivered, email.delivery_delayed, email.bounced, email.complained, email.opened, email.clicked, email.failed, email.unsubscribed, email.received.

MCP server

Connect Cursor, Claude Code, VS Code, Codex or any other MCP client to https://mailumi.com/mcp and your assistant can send a test email, check whether it was delivered and see which domains are verified, without leaving the editor. The server uses your API keys and their permissions, so a key without email:send gives read-only access.

{
  "mcpServers": {
    "mailumi": {
      "url": "https://mailumi.com/mcp",
      "headers": {
        "Authorization": "Bearer ${env:MAILUMI_API_KEY}"
      }
    }
  }
}
  • Cursor: Add to Cursor in one click, or save the JSON in ~/.cursor/mcp.json.
  • Claude Code: run the command in your terminal.
  • VS Code: save the JSON as .vscode/mcp.json; VS Code asks for the key once and stores it securely.
  • Codex: add the lines to ~/.codex/config.toml.

Set MAILUMI_API_KEY in your system environment before you open the editor. You can also put the key straight into your user-level config instead of the variable, but never into a file you commit.

ToolPermissionWhat it does
send_emailemail:sendSend an email, for example a test to your own address
list_emailsemail:readList recent sent and received email
get_emailemail:readCheck delivery: each recipient’s status and every event
reschedule_emailemail:sendChange when a scheduled email is sent
cancel_emailemail:sendCancel a scheduled email
list_domainsdomains:readList your domains and their DNS verification status
get_documentationAny keyRead the complete API reference as Markdown

Emails sent through MCP are ordinary emails: they count toward your plan and appear in Email activity. The server speaks the Streamable HTTP transport, and requests count toward the same 120 per minute limit as the API.

Authentication and base URL

All endpoints live under https://mailumi.com/api/v1. Create a key in Dashboard → API keys, choose the permissions it needs and, if you like, restrict it to one domain or give it an expiry date. The full key is shown once; store it straight away.

Send the key in an Authorization: Bearer header from your backend, never from browser or mobile code. The permissions are email:send, email:read, domains:read and contacts:write (audiences and contacts). Revoke a key at any time in the dashboard.

Send an email

Send a JSON body with from, to, subject, and text, html or both. The sender’s domain must be verified in your account. Optional fields are cc, bcc, reply_to, attachments, headers, tags and scheduled_at.

Request
POST https://mailumi.com/api/v1/emails
Authorization: Bearer YOUR_MAILUMI_API_KEY
Content-Type: application/json
Idempotency-Key: welcome-user-123

{
  "from": "Acme <hello@yourdomain.com>",
  "to": "alex@example.com",
  "subject": "Welcome aboard",
  "html": "<p>Your account is ready.</p>",
  "text": "Your account is ready.",
  "tags": [{ "name": "category", "value": "welcome" }]
}

The response is the new email, with its id and status queued. Mailumi sends it within seconds and records every following event.

Response
{
  "id": "…",
  "object": "email",
  "domain_id": "…",
  "direction": "outbound",
  "from": "hello@yourdomain.com",
  "to": ["alex@example.com"],
  "subject": "Welcome aboard",
  "status": "queued",
  "last_event": "queued",
  "scheduled_at": null,
  "tags": { "category": "welcome" },
  "error": null,
  "created_at": "2026-10-06T09:00:00.000Z"
}

The optional Idempotency-Key header (1–256 characters) protects against duplicates for 24 hours: if a request times out, repeat it with the same key and Mailumi returns the original email instead of sending twice. Reusing a key with different content returns 409.

An attachment is an object with filename, base64 content, an optional content_type and an optional content_id to show it inline in HTML with <img src="cid:…">. headers is an object of extra headers such as List-Unsubscribe. tags are up to 10 name and value pairs of letters, numbers, underscores and dashes; they are returned with the email and in every webhook event.

Send a batch

Send up to 100 emails in one request with POST /emails/batch and a JSON array of email objects. The batch is accepted or rejected as a whole: if one email is invalid or the batch exceeds your plan limit, nothing is sent and the error names the email. Batch emails cannot have attachments or scheduled_at.

HTTP
POST https://mailumi.com/api/v1/emails/batch
Authorization: Bearer YOUR_MAILUMI_API_KEY
Content-Type: application/json

[
  { "from": "hello@yourdomain.com", "to": "alex@example.com", "subject": "Your receipt", "text": "…" },
  { "from": "hello@yourdomain.com", "to": "sam@example.com", "subject": "Your receipt", "text": "…" }
]

→ { "data": [{ "id": "…" }, { "id": "…" }] }

Schedule, reschedule and cancel

Add scheduled_at to send later, up to 30 days ahead. Use an ISO 8601 date such as 2026-10-07T09:00:00Z or plain English such as in 1 hour or in 3 days. The email gets the status scheduled until it is sent.

Change the time with PATCH /emails/:id and a new scheduled_at, or cancel with POST /emails/:id/cancel. Canceling returns the email to your plan allowance. Only scheduled emails that have not been sent can be changed.

Send with a template

Create a template in Dashboard → Templates with placeholders such as {{first_name}}, then pass its ID and the values instead of subject and html. Values are HTML-escaped, and any variable you leave out uses the fallback saved with the template. A subject in the request overrides the template’s subject.

JSON
{
  "from": "Acme <hello@yourdomain.com>",
  "to": "alex@example.com",
  "template_id": "tpl_…",
  "variables": { "first_name": "Alex", "login_url": "https://app.acme.com" }
}

Campaigns and contacts

Campaigns are newsletters and announcements sent to an audience, a list of contacts. Write and send them in Dashboard → Campaigns. Each recipient counts as one email toward your plan, and every campaign email shows up in Email activity with its own delivery events.

  • Add contacts in Dashboard → Audiences one by one, by pasting a list, or by importing a CSV from another tool. Columns named email, first name and last name are recognized automatically.
  • Personalize the subject and message with {{first_name}}, {{last_name}} and {{email}}.
  • Every campaign email gets your workspace name, your postal address and an unsubscribe link in the footer, plus List-Unsubscribe and List-Unsubscribe-Post headers so Gmail, Yahoo and Apple Mail show their one-click unsubscribe button. Add the postal address in Dashboard → Settings.
  • An unsubscribe applies to the address in every audience of the workspace and fires the email.unsubscribed webhook event. Importing a list never resubscribes anyone, and suppressed addresses are skipped.
  • Campaigns are sent at a steady pace on a separate queue, so they never delay your transactional email.

Keep audiences in sync from your app with a key that has the contacts:write permission. Add one contact with an object, or import up to 1,000 at once with an array:

Shell
curl -X POST https://mailumi.com/api/v1/audiences/aud_…/contacts \
  -H "Authorization: Bearer $MAILUMI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "email": "alex@example.com", "first_name": "Alex" }'

To unsubscribe someone who opted out in your app, send PATCH /audiences/:id/contacts/alex%40example.com with { "unsubscribed": true }. Only send campaigns to people who agreed to receive them.

Endpoints

Method & pathPermissionPurpose
POST /emailsemail:sendSend an email
POST /emails/batchemail:sendSend up to 100 emails at once
PATCH /emails/:idemail:sendChange the send time of a scheduled email
POST /emails/:id/cancelemail:sendCancel a scheduled email
GET /emailsemail:readList sent and received email
GET /emails/:idemail:readRead one email with per-recipient status and its events
GET /emails/:id/attachments/:indexemail:readDownload an attachment
GET /domainsdomains:readList your domains and their verification status
GET /audiencescontacts:writeList audiences with contact counts
POST /audiencescontacts:writeCreate an audience
PATCH /audiences/:idcontacts:writeRename an audience
DELETE /audiences/:idcontacts:writeDelete an audience and its contacts
GET /audiences/:id/contactscontacts:writeList or search contacts, 100 per page
POST /audiences/:id/contactscontacts:writeAdd one contact, or import up to 1,000
GET /audiences/:id/contacts/:contactcontacts:writeRead a contact by ID or email address
PATCH /audiences/:id/contacts/:contactcontacts:writeChange a name or unsubscribe
DELETE /audiences/:id/contacts/:contactcontacts:writeRemove a contact

Paths are relative to https://mailumi.com/api/v1. Filter the list with direction=inbound or direction=outbound. Results come 50 at a time as { "data": […], "next_cursor": … }; pass next_cursor as the before parameter to load the next page.

The OpenAPI 3.1 description lists every endpoint with its operation ID, request and response schemas and errors. Use it to generate a client or to give an AI agent the API as function calls.

Statuses and events

Every email has a status and a last_event, and each recipient has its own status. GET /emails/:id returns the full event history.

EventWhen
email.sentThe email left Mailumi for the recipient’s mail server
email.deliveredThe recipient’s mail server accepted it
email.delivery_delayedThe receiving server is temporarily refusing it and delivery is being retried
email.bouncedThe address does not exist or refused the email; hard bounces are suppressed
email.complainedThe recipient marked the email as spam; the address is suppressed
email.openedThe recipient opened the email (open tracking on)
email.clickedThe recipient clicked a link (click tracking on)
email.failedThe email could not be sent; the reason is included
email.unsubscribedThe recipient unsubscribed from a campaign, by its link or the email app’s unsubscribe button
email.receivedAn email arrived on one of your receiving domains

Turn on open and click tracking per domain in Dashboard → Domains; both are off by default. Addresses that hard-bounce or complain are added to your suppression list in Dashboard → Suppressions. Later emails to them are accepted but not sent, do not count toward your plan, and show the recipient status suppressed.

Webhooks

Add an HTTPS endpoint in Dashboard → Webhooks and choose its events. Each delivery is a JSON object with id, type, created_at and data; data.email_id identifies the email. email.received includes the sender, recipients, subject, text, HTML and attachment links, which you download with a read key. Press Send test for a webhook.test event.

Event
{
  "id": "…",
  "type": "email.delivered",
  "created_at": "2026-10-06T09:00:04.000Z",
  "data": {
    "email_id": "…",
    "domain_id": "…",
    "direction": "outbound",
    "from": "hello@yourdomain.com",
    "to": ["alex@example.com"],
    "subject": "Welcome aboard",
    "status": "delivered",
    "last_event": "delivered",
    "scheduled_at": null,
    "tags": { "category": "welcome" },
    "error": null,
    "created_at": "2026-10-06T09:00:00.000Z",
    "recipient": "alex@example.com",
    "delivery": { "smtp_response": "250 2.0.0 OK" }
  }
}

Mailumi signs every delivery with the Standard Webhooks format, so any Standard Webhooks library can verify it. Read the raw body before parsing JSON, compute HMAC-SHA256 over webhook-id.webhook-timestamp.body with the base64-decoded part of your whsec_ secret, and compare it in constant time with the value after v1,. Reject old timestamps and store event IDs so you process each event once.

// npm install mailumi
import { verifyWebhook } from 'mailumi';

// Pass the raw body before parsing JSON: the signature covers the exact bytes.
const { data: event, error } = await verifyWebhook({
  payload: await request.text(),
  headers: request.headers,
  secret: process.env.MAILUMI_WEBHOOK_SECRET, // whsec_…
});
if (error) return new Response('Invalid signature', { status: 401 });

// Save event.id with a unique constraint so each event is processed once.
await saveEventOnce(event.id, event);
return new Response('OK');

Respond with any 2xx status within eight seconds. If your endpoint fails, Mailumi retries automatically with increasing delays, up to ten attempts within 24 hours, using the same event ID. Every attempt is visible in the dashboard, where you can also retry by hand.

Limits and errors

LimitValue
Recipients (to, cc and bcc)50 per email
Batch100 emails per request
Attachments20 per email, 10 MB in total
Text and HTML combined500 KB
Request body15 MB
SchedulingUp to 30 days ahead
API requests120 per minute per key
Requests with a missing or invalid key30 per minute per IP address
Audiences100 per workspace, 100,000 contacts each
Contact import1,000 contacts per request
Free plan3,000 emails per month, 100 per day

Errors are JSON with a message that explains what to fix and a machine-readable code, for example { "error": "Send to no more than 50 recipients per email.", "code": "invalid_request" }.

StatusCodeMeaning
400invalid_requestThe request is invalid; the message explains what to fix
401unauthorizedThe API key is missing, invalid, expired or revoked
402plan_limit_reachedYour plan’s sending limit is reached
403forbiddenThe key lacks the permission, the domain is not verified, or the key may not use this domain
404not_foundThe email, attachment, template, audience, contact or endpoint was not found
409conflictThe idempotency key was already used with different content, the email can no longer be changed, or a campaign is being sent to the audience
413payload_too_largeThe request body is larger than 15 MB, or a contact import is larger than 1 MB
429rate_limitedToo many requests; wait the number of seconds in Retry-After
503service_unavailableA temporary problem; retry with the same idempotency key

Every API response includes RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds), so your code can slow down before it hits the limit. A 429 response also includes Retry-After.

Each recipient counts as one email toward your plan. Incoming email does not count. See delivery monitoring for what each delivery status means.

Versioning and deprecation policy

The version is part of the path: /api/v1. Version 1 gets no breaking changes. New endpoints, new optional request fields, new response fields and new event types can be added at any time, so ignore fields you do not know.

A breaking change only ships in a new version, such as /api/v2. When that happens, version 1 keeps working for at least 12 months after the new version is released. Every workspace owner gets an email at least 6 months before an endpoint is retired, and responses from a deprecated endpoint carry a Deprecation header and a Sunset header with the retirement date.

Send your first email today.

Get your free API keyAsk an integration question

3,000 free emails every month. No credit card required.