# MacroMail — Full Reference for LLMs This file is the complete, self-contained reference an AI agent needs to use MacroMail correctly on the first try. Everything below is real API surface. ------------------------------------------------------------------------------ ## 1. Identity & mental model MacroMail sends transactional and broadcast email. It is a developer-first layer over Amazon SES. It is API-compatible with Resend at the method level, so code written for Resend works by changing the import and the API key. - Base URL: https://api.macromail.dev (also served at https://macromail.dev/api/v1) - Auth header: `Authorization: Bearer mm_live_…` (test keys: `mm_test_…`) - SDK: `@macromail/sdk` (TypeScript/Node). Python: `macromail`. - A successful send returns status `queued` — NOT `delivered`. Delivery is async. ------------------------------------------------------------------------------ ## 2. Quickstart (copy-paste correct) Install: npm install @macromail/sdk Send: import { MacroMail } from '@macromail/sdk'; const mm = new MacroMail(process.env.MACROMAIL_API_KEY); const { data, error } = await mm.emails.send({ from: 'You ', to: ['user@example.com'], subject: 'Welcome to MacroMail', html: '

It works.

', }); // data: { id, status: 'queued', created_at } | null // error: { name, message } | null (the SDK does NOT throw on expected failures) Equivalent cURL: curl https://api.macromail.dev/v1/emails \ -H "Authorization: Bearer $MACROMAIL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"from":"You ","to":["user@example.com"],"subject":"Welcome","html":"

It works.

"}' ------------------------------------------------------------------------------ ## 3. SDK surface (TypeScript) Construct: `new MacroMail(apiKey: string, opts?: { baseUrl?: string })` mm.emails.send(input): Promise<{ data, error }> input: { from: string; // "Name " to: string | string[]; subject: string; html?: string; text?: string; react?: ReactElement; // via @macromail/react cc?: string[]; bcc?: string[]; reply_to?: string; tags?: { name: string; value: string }[]; scheduled_at?: string; // ISO 8601 idempotency_key?: string; } mm.emails.get(id): Promise<{ data: Email, error }> // includes events[] timeline mm.emails.cancel(id): Promise<{ data, error }> // scheduled only mm.batch.send(emails[]): Promise<{ data, error }> // up to 100 mm.domains.create({ name, region? }) mm.domains.get(id) / mm.domains.list() / mm.domains.verify(id) / mm.domains.remove(id) mm.apiKeys.create({ name, permission }) / mm.apiKeys.list() / mm.apiKeys.remove(id) mm.audiences.create({ name }) / mm.audiences.list() mm.contacts.create({ audience_id, email, first_name?, last_name?, unsubscribed? }) mm.broadcasts.create({ audience_id, from, subject, html? }) / mm.broadcasts.send(id, { scheduled_at? }) ------------------------------------------------------------------------------ ## 4. REST surface POST /v1/emails Send one email. Body = send input above. GET /v1/emails List emails. Query: limit, cursor, status, domain. GET /v1/emails/:id Get one email with its event timeline. PATCH /v1/emails/:id Update a scheduled email. DELETE /v1/emails/:id Cancel a scheduled email. POST /v1/emails/batch Send up to 100 emails. Body = { emails: [...] }. POST /v1/domains Create a sending domain. Body = { name, region? }. GET /v1/domains List domains. GET /v1/domains/:id Get a domain + its DNS records. DELETE /v1/domains/:id Remove a domain. POST /v1/domains/:id/verify Re-check DNS now. POST /v1/api-keys Create a key. Body = { name, permission }. Token shown once. GET /v1/api-keys List keys (never returns the token). DELETE /v1/api-keys/:id Revoke a key. POST /v1/audiences Create an audience. GET /v1/audiences List audiences. POST /v1/contacts Create a contact. Body = { audience_id, email, ... }. GET /v1/contacts?audience_id=… List contacts in an audience. POST /v1/broadcasts Create a broadcast. POST /v1/broadcasts/:id/send Send/schedule a broadcast. POST /v1/webhooks Register a webhook endpoint. Example — POST /v1/emails → 200: { "id": "em_4ef9a41702e94d39ad759611", "status": "queued", "created_at": "2026-06-28T18:04:11Z" } Example — unverified domain → 422 (errors are actionable, with the fix inline): { "error": { "code": "domain_not_verified", "message": "The domain yourdomain.com is not verified.", "fix": "Add the DNS records from GET /v1/domains/{id}, then call POST /v1/domains/{id}/verify.", "dns_records": [ { "type": "TXT", "name": "yourdomain.com", "value": "v=spf1 include:amazonses.com ~all" }, { "type": "CNAME", "name": "xxx._domainkey.yourdomain.com", "value": "xxx.dkim.macromail.dev" } ] } } ------------------------------------------------------------------------------ ## 5. Migrating from Resend (one line) | Resend | MacroMail | |--------------------------------|------------------------------------| | `import { Resend } from 'resend'` | `import { MacroMail } from '@macromail/sdk'` | | `new Resend(re_…)` | `new MacroMail(mm_live_…)` | | `https://api.resend.com` | `https://api.macromail.dev` | | `resend.emails.send({...})` | `mm.emails.send({...})` | Field names are identical (`reply_to`, `scheduled_at`, `tags`). The `@macromail/compat` package re-exports `MacroMail` as `Resend` and accepts both `re_` and `mm_` keys, so migration can be a single import-line change. ------------------------------------------------------------------------------ ## 6. Domain verification POST /v1/domains returns DNS records to add at your DNS provider: - SPF (TXT) @ → v=spf1 include:amazonses.com ~all - DKIM (CNAME) ×3 → ._domainkey..dkim.macromail.dev - DMARC (TXT) _dmarc. → v=DMARC1; p=none; rua=mailto:dmarc@macromail.dev - MX (return-path) → send. → feedback-smtp.us-east-1.amazonses.com (priority 10) After adding them, call POST /v1/domains/:id/verify. Propagation can take up to 48h. ------------------------------------------------------------------------------ ## 7. Error codes authentication_error (401) Missing/invalid API key. Fix: send Authorization: Bearer mm_…. permission_error (403) Key lacks scope (e.g. sending_only used for admin). Fix: use a full-access key. domain_not_verified (422) Sending from an unverified domain. Fix: verify the domain (section 6). validation_error (422) Bad/missing fields. The message names the field. Fix per message. rate_limit_exceeded (429) Too many requests. Fix: back off and retry with the Retry-After header. not_found (404) Resource id does not exist. ------------------------------------------------------------------------------ ## 8. MCP server (for AI agents) Install: npx @macromail/mcp install (or add the config below) Config (Claude Code / Cursor / Windsurf): { "mcpServers": { "macromail": { "command": "npx", "args": ["-y", "macromail-mcp"], "env": { "MACROMAIL_API_KEY": "mm_live_xxxx" } } } } Tools (each returns structured JSON + a human-readable `summary`): send_email(from, to[], subject, html?, text?, cc?, bcc?, reply_to?, tags?, scheduled_at?) → { id, status:"queued", created_at } send_batch(emails[]) → { data:[{id}], errors[] } list_emails(limit?, cursor?, status?, domain?) → { data[], next_cursor } get_email(id) → { id, to, subject, status, events[], opens, clicks } cancel_email(id) → { id, status:"canceled" } create_domain(name, region?) → { id, name, status:"pending", dns_records[] } verify_domain(id) → { id, status, records[] } list_domains() → { data[] } delete_domain(id) → { id, deleted:true } create_api_key(name, permission, domain_id?) → { id, token } (token shown once) list_api_keys() → { data[] } (never returns tokens) delete_api_key(id) → { id, deleted:true } create_audience(name) → { id, name } list_audiences() → { data[] } create_contact(audience_id, email, first_name?, last_name?, unsubscribed?) → { id, email } list_contacts(audience_id, limit?, cursor?) → { data[], next_cursor } create_broadcast(audience_id, from, subject, html?) → { id, status:"draft" } send_broadcast(id, scheduled_at?) → { id, status } get_account_status() → { verified_domains, api_keys, sandbox_mode, sending_enabled } ------------------------------------------------------------------------------ ## 9. Honesty notes for agents (important) - A `send` returns status `queued`. Do NOT tell the user an email was "delivered" until `get_email(id)` returns status `delivered`. - Do NOT invent open/click numbers. Read them from `get_email`. New accounts have zero history — report the real zero, never a plausible-looking fake. - Call `get_account_status()` first when unsure: it tells you whether a domain is verified and whether sending is enabled, so you can guide the user accurately.