Docs
Quickstart
Get started with Mailodee: verify a domain, send your first email, receive replies, send marketing with one-click unsubscribe, and get signed webhooks.
1Create an account and pick a plan
Sign up, then pick a plan on the Billing page: Indie for up to 10 domains, or Builder for unlimited domains and 100,000 emails a month. You can add domains and API keys before paying; sending and receiving turn on when you subscribe.
New accounts can send to 100 recipients a day. The limit doubles automatically while your bounce and complaint rates stay clean, up to 2,000 a day on Indie and 3,000 on Builder.
2Add and verify a domain
Use a subdomain such as mail.yourapp.com for sending, so your root domain's reputation stays separate. Add it under Domains in the dashboard, or with the API.
You get three DKIM CNAME records, an MX and TXT record for the bounce subdomain, and a suggested DMARC record. Add them at your DNS provider, then press Check now. DKIM usually verifies within minutes, sometimes a few hours.
curl https://mailodee.com/v1/domains \
-H "Authorization: Bearer mk_..." \
-H "Content-Type: application/json" \
-d '{ "name": "mail.yourapp.com" }'3Send an email
Create an API key under API keys (one per app is easiest to revoke), then call POST /v1/emails. The response comes back right away with status queued; delivery, bounces and complaints show up in Logs and on webhooks.
Send an Idempotency-Key header when you retry, so a timeout never sends twice.
curl https://mailodee.com/v1/emails \
-H "Authorization: Bearer mk_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: welcome-user-42" \
-d '{
"from": "Your App <hello@mail.yourapp.com>",
"to": "maya@example.com",
"subject": "Welcome aboard",
"html": "<p>Glad you are here.</p>",
"text": "Glad you are here."
}'
→ 202 { "id": "cm...", "status": "queued" }4Receive email
Turn on inbound for the domain under Domains, then add the MX record it shows. Use a subdomain like reply.yourapp.com: pointing your root domain's MX here moves all of its mail and breaks an existing inbox.
Add a route under Routes. Use * as the catch-all, and choose what happens to each message: store it in the inbox, forward it to another address, send it to a webhook, or drop it.
curl https://mailodee.com/v1/routes \
-H "Authorization: Bearer mk_..." \
-H "Content-Type: application/json" \
-d '{
"domain_id": "cm...",
"match": "*",
"actions": [{ "type": "store" }, { "type": "forward", "to": "you@gmail.com" }]
}'5Send marketing email (optional)
Newsletters and product updates are fine to people who opted in. Add "category": "marketing" and send one recipient per email. Mailodee adds a one-click List-Unsubscribe header and a footer link, and suppresses anyone who uses them, so you never mail them again by accident.
Marketing goes through its own SES configuration set, so its complaints are tracked apart from your transactional mail. Subscribe a webhook to email.unsubscribed to update your own list.
curl https://mailodee.com/v1/emails \
-H "Authorization: Bearer mk_..." \
-H "Content-Type: application/json" \
-d '{
"from": "Your App <news@mail.yourapp.com>",
"to": "maya@example.com",
"subject": "What we shipped in November",
"html": "<p>Three new features...</p>",
"category": "marketing"
}'6Get webhooks
Add an endpoint under Webhooks and copy its signing secret (shown once). Every event is signed with HMAC-SHA256 in the Mailodee-Signature header. Verify it and reject timestamps older than five minutes.
Failed deliveries are retried for about 24 hours. Deliveries are at least once, so de-duplicate on the event id.
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyMailodeeSignature(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
const isFresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return isFresh && timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
}API at a glance
Every request needs Authorization: Bearer mk_.... Errors always look like { "error": { "code": "...", "message": "..." } }, so your code can switch on the code.