# FutureMail developer docs

FutureMail is email hosting with an API. Everything the web inbox can do, your code and your AI agents can do over **REST**, the **MCP server**, or a **Resend-compatible API**. Domains, addresses, sending, receiving, searching, threads, webhooks: same data, same permissions.

- **API base URL:** `https://api.futuremail.dev`
- **MCP server (Streamable HTTP):** `https://api.futuremail.dev/mcp`
- **Resend-compatible API:** `https://api.futuremail.dev/resend`
- **Auth:** `Authorization: Bearer fm_live_…`

## Quickstart

1. [Sign up](https://futuremail.dev/signup). You get a free `you@futuremail.dev` address instantly, with no DNS to configure.
2. Create an API key in **Settings → API keys**.
3. Send an email:

```bash
curl -X POST https://api.futuremail.dev/v1/emails \
  -H "Authorization: Bearer $FUTUREMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "you@futuremail.dev",
    "to": "delivered@test.futuremail.dev",
    "subject": "Hello from FutureMail",
    "text": "It works."
  }'
```

`delivered@test.futuremail.dev` is a [test address](#test-addresses): it simulates a delivery without sending real mail and doesn't count toward your limits.

## Authentication

Every request carries an API key: `Authorization: Bearer fm_live_…`. Keys belong to an **organization** (your personal org or a team) and only see that org's data.

| Scope | Allows |
| --- | --- |
| `full` | Everything, including domains, addresses, keys and webhooks |
| `send` | Sending, replying, forwarding, drafts |
| `read` | Listing, reading, searching, waiting, updating thread state |

Keys can be **pinned to one mailbox** (agent keys always are), limited to **one sending domain**, and given an **expiry**. Keys never manage the org itself (members, invites, billing); that needs a signed-in member.

## Sending email

`POST /v1/emails`

| Field | Notes |
| --- | --- |
| `from` | An address you own in FutureMail |
| `to`, `cc?`, `bcc?`, `replyTo?` | `"ada@x.com"`, `"Ada <ada@x.com>"`, `{ address, name }`, or an array of these |
| `subject` | Required unless a template provides it |
| `text?`, `html?` | Plain text and/or HTML body |
| `attachments?` | `[{ filename, content (base64) \| path (public URL), contentType?, contentId? }]`. `contentId` makes it inline (`<img src="cid:logo">`). Max 20 attachments, 10 MB per message |
| `headers?` | Custom headers such as `List-Unsubscribe` |
| `tags?` | `[{ name, value }]`, returned on the email, in webhooks, and filterable |
| `scheduledAt?` | ISO 8601, up to 30 days ahead. Reschedule with `PATCH /v1/emails/:id`, cancel with `POST /v1/emails/:id/cancel` |
| `template?` | `{ id, variables }`: render a published template (id or alias) |

- **Batch:** `POST /v1/emails/batch` sends up to 100 emails in one call, validated all-or-nothing.
- **Idempotency:** send an `Idempotency-Key` header (≤256 chars). Retrying with the same key within 24 hours returns the original response instead of sending twice.
- **Delivery timeline:** an email moves `queued` → `sent` → `delivered`, or ends `bounced` / `complained` / `failed`. `GET /v1/emails/:id/events` returns every event, including `opened` and `clicked` when tracking is on.
- **Replies and forwards:** `POST /v1/emails/:id/reply` (`{ text?, html?, replyAll? }`) threads correctly and quotes the original; `POST /v1/emails/:id/forward` (`{ to, text? }`) includes attachments.

### Test addresses

Send to `<outcome>[+label]@test.futuremail.dev` to simulate an outcome without sending anything:

| Address | Result |
| --- | --- |
| `delivered@test.futuremail.dev` | `delivered` |
| `bounced@test.futuremail.dev` | `bounced` (permanent) |
| `complained@test.futuremail.dev` | `delivered`, then `complained` |
| `delayed@test.futuremail.dev` | `delivery_delayed`, then `delivered` |

Test sends don't count toward limits and can't be mixed with real recipients in one email.

## Agent inboxes

Give every AI agent its own mailbox, address and API key:

```bash
curl -X POST https://api.futuremail.dev/v1/agents \
  -H "Authorization: Bearer $FUTUREMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Signup bot" }'
# → { "mailbox": {…}, "address": {…}, "apiKey": { "id": "…", "key": "fm_live_…", … } }
# apiKey.key is shown once: store it now
```

The agent's key is pinned to its mailbox: it can only read and send from its own inbox.

### Waiting for an email (OTP and magic links)

`GET /v1/emails/wait` long-polls until a matching inbound email arrives. Filter with `from`, `to`, `subject` and `since`; `timeout` is in seconds (≤300). It returns the email, or `204` on timeout. One-time codes and links are extracted for you:

```bash
curl "https://api.futuremail.dev/v1/emails/wait?from=github.com&timeout=120" \
  -H "Authorization: Bearer $AGENT_KEY"
# → { …, "extracted": { "otp": "481516", "links": ["https://github.com/…"] } }
```

### Reading and searching

- `GET /v1/threads?folder=inbox` lists conversations (`inbox`, `starred`, `sent`, `all`, `spam`, `trash`); `GET /v1/threads/:id` returns a thread with all its messages; `PATCH /v1/threads/:id` archives, stars, trashes or marks read.
- `GET /v1/emails?q=…` searches messages with Gmail-style operators: `from:` `to:` `subject:` `has:attachment` `is:unread` `is:starred` `in:inbox|sent|spam|trash` `before:2026-10-01` `after:2026-09-01` `newer_than:7d` `older_than:1m`. Quote values with spaces: `subject:"weekly report"`.
- `GET /v1/emails/:id/raw` returns the RFC 822 source; `GET /v1/attachments/:id` downloads an attachment.

## MCP server

FutureMail runs a remote MCP server over Streamable HTTP. Add it to any MCP client (Claude, Claude Code, Cursor, …):

```json
{
  "mcpServers": {
    "futuremail": {
      "type": "http",
      "url": "https://api.futuremail.dev/mcp",
      "headers": { "Authorization": "Bearer fm_live_…" }
    }
  }
}
```

With Claude Code:

```bash
claude mcp add --transport http futuremail https://api.futuremail.dev/mcp \
  --header "Authorization: Bearer fm_live_…"
```

Tools: `whoami`, `list_inbox`, `search_emails`, `read_thread`, `read_email`, `send_email`, `send_batch`, `get_email_events`, `cancel_scheduled_email`, `reschedule_email`, `list_templates`, `reply_to_email`, `forward_email`, `update_thread`, `wait_for_email`, `list_domains`, `add_domain`, `verify_domain`, `create_address`, `create_agent_inbox`.

Use an agent key to confine the model to one mailbox.

## Resend compatibility

FutureMail serves the same API in Resend's format under `https://api.futuremail.dev/resend`. Request bodies, responses and errors match Resend's, so moving over is a base URL and key change:

```bash
RESEND_BASE_URL=https://api.futuremail.dev/resend
RESEND_API_KEY=fm_live_…
```

```ts
import { Resend } from "resend";

const resend = new Resend("fm_live_…", { baseUrl: "https://api.futuremail.dev/resend" });

await resend.emails.send({
  from: "Acme <hello@acme.com>", // an address you own in FutureMail
  to: ["delivered@test.futuremail.dev"],
  subject: "Hello",
  html: "<p>It works</p>",
});
```

Supported: `POST /emails`, `POST /emails/batch`, `GET /emails`, `GET /emails/:id`, `PATCH /emails/:id`, `POST /emails/:id/cancel`, domains and API keys. Other Resend SDKs (Python, Go, PHP, Ruby, …) work if they let you override the base URL. Webhooks use FutureMail's signature format (below), not Svix's.

## Webhooks

Create webhooks in **Settings → Developers** or with `POST /v1/webhooks` (`{ url, events }`; the signing secret is returned once).

Events: `email.received`, `email.scheduled`, `email.sent`, `email.delivery_delayed`, `email.delivered`, `email.bounced`, `email.complained`, `email.failed`, `email.suppressed`, `email.opened`, `email.clicked`, `domain.verified`.

Payload: `{ "id": "evt_…", "type": "email.delivered", "createdAt": "…", "data": { … } }`. The `id` is stable across retries, so use it to dedupe.

Every delivery is signed: `FutureMail-Signature: t=<unix>,v1=<hex>` where `v1 = HMAC_SHA256(secret, "<t>.<raw body>")`. Reject requests whose `t` is more than 5 minutes old:

```ts
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyFutureMail(rawBody: string, header: string, secret: string) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=") as [string, string]));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const received = Buffer.from(parts.v1 ?? "", "hex");
  return received.length === expected.length / 2 && timingSafeEqual(received, Buffer.from(expected, "hex"));
}
```

Non-2xx responses and timeouts (10s) are retried, up to 8 attempts over about 28 hours. Replay any delivery with `POST /v1/webhooks/:id/deliveries/:deliveryId/replay`. For a live feed instead, `GET /v1/events` is a Server-Sent Events stream of the same events.

## Domains and addresses

On Pro you can use any domain you own:

1. `POST /v1/domains` with `{ "name": "acme.com" }` returns the DNS `records[]` to add: DKIM (three CNAMEs), MX for receiving, a custom MAIL FROM (MX + SPF TXT) and a recommended DMARC record.
2. Add them at your DNS provider, then `POST /v1/domains/acme.com/verify`.
3. Create addresses: `POST /v1/addresses` with `{ "address": "hello@acme.com" }`. Addresses are free and unlimited; set a catch-all per domain, and plus-addressing (`me+tag@acme.com`) always works.

Per-domain settings (`PATCH /v1/domains/:idOrName`): `catchAllMailboxId`, `mailFromSubdomain`, `tlsPolicy`, `openTracking`, `clickTracking`, `trackingSubdomain`.

## Templates and suppressions

- **Templates:** `POST /v1/templates` with `{ name, alias?, subject, html?, text?, variables? }`, then `POST /v1/templates/:idOrAlias/publish`. Placeholders are `{{name}}` (HTML-escaped) and `{{{name}}}` (raw). Send with `template: { id, variables }`.
- **Suppressions:** permanent bounces and spam complaints are suppressed automatically. Manage the list with `GET/POST /v1/suppressions`, `DELETE /v1/suppressions/:email`, and `POST /v1/suppressions/check`.

## Endpoint reference

| Endpoint | Purpose |
| --- | --- |
| `POST /v1/emails`, `POST /v1/emails/batch` | Send one or up to 100 emails |
| `GET /v1/emails`, `GET /v1/emails/:id` | List/search and read messages |
| `GET /v1/emails/wait` | Long-poll for the next matching inbound email |
| `GET /v1/emails/:id/events` | Delivery timeline |
| `PATCH /v1/emails/:id`, `POST /v1/emails/:id/cancel` | Reschedule or cancel a scheduled email |
| `POST /v1/emails/:id/reply`, `POST /v1/emails/:id/forward` | Reply in-thread, forward |
| `GET/PATCH/DELETE /v1/threads/:id`, `POST /v1/threads/batch` | Conversations |
| `GET/POST /v1/domains`, `POST /v1/domains/:idOrName/verify` | Custom domains and DNS |
| `GET/POST /v1/addresses`, `GET /v1/addresses/availability` | Addresses and aliases |
| `GET/POST /v1/mailboxes`, `POST /v1/agents` | Mailboxes and agent inboxes |
| `GET/POST /v1/templates`, `POST /v1/templates/:id/publish` | Templates |
| `GET/POST /v1/suppressions` | Suppression list |
| `GET/POST /v1/api-keys` | API keys |
| `GET/POST /v1/webhooks`, `GET /v1/events` | Webhooks and the SSE event stream |
| `GET /v1/me` | Current org, role, plan and usage |

## Errors, pagination and rate limits

- **Errors:** `{ "error": { "code": "not_found", "message": "Email not found" } }` with a matching HTTP status. Paid-plan actions on Free return `402 upgrade_required`.
- **Pagination:** lists return `{ "data": [...], "nextCursor": "…" | null }` and accept `limit` (1–100) and `cursor`.
- **Rate limits:** 10 requests/second per API key. Responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`; over the limit you get `429` with `Retry-After`, and the request was not processed, so it is safe to retry.

## Plans and limits

| | Free | Pro ($20/month) |
| --- | --- | --- |
| Addresses on futuremail.dev | ✓ | ✓ |
| Agent inboxes, API, MCP, webhooks | ✓ | ✓ |
| Custom domains and addresses on them | – | Unlimited |
| Recipients per month | 3,000 (100 a day) | 50,000, no daily cap |

Limits are hard caps: you're never charged overage, and there's no per-seat pricing. At most 50 recipients and 10 MB per message.
