# MX Forwarder API Documentation

> Machine-readable API reference for AI agents and developers.
> OpenAPI 3.1 spec: https://mx-forwarder.com/docs/openapi.json

## Table of Contents

1. [What is MX Forwarder?](#what-is-mx-forwarder)
2. [Authentication](#authentication)
3. [Rate Limits](#rate-limits)
4. [Endpoints — Addresses](#addresses)
5. [Endpoints — Emails](#emails)
6. [Endpoints — Deliveries](#deliveries)
7. [Endpoints — Usage](#usage)
8. [Endpoints — Reseller](#reseller)
9. [Webhooks](#webhooks)
10. [Webhook Signature Verification](#webhook-signature-verification)
11. [Pagination](#pagination)
12. [Error Handling](#error-handling)
13. [MCP Server](#mcp-server)
14. [Reseller Integration Guide](#reseller-integration-guide)

---

## What is MX Forwarder?

MX Forwarder is an **inbound email-to-webhook service**. It lets you receive emails at generated addresses (e.g. `invoices@mx-forwarder.com`) and get them delivered as structured JSON to any HTTP endpoint.

**Key concepts:**
- **Address** — A unique email address (e.g. `invoices@mx-forwarder.com`) that forwards to your webhook URL
- **Webhook** — When an email arrives, MX Forwarder parses it (headers, body, attachments, SPF/DKIM/DMARC) and POSTs the structured payload to your URL, signed with HMAC-SHA256
- **Delivery** — Each webhook attempt is tracked. Failed deliveries are retried with exponential backoff (up to 100 attempts)
- **Reseller** — Create sub-accounts for your users, track per-customer usage, get billed for aggregate consumption

**Common use cases:** invoice processing, customer support intake, email-triggered workflows, CRM integrations, newsletter forwarding, IoT device alerts.

## Authentication

Base URL: `https://mx-forwarder.com`

All API requests require a Bearer token:

```
Authorization: Bearer mf_live_<your_api_key>
```

Generate an API key at https://mx-forwarder.com/dashboard/api-keys

All `/api/*` endpoints always return JSON responses (never HTML). If you get a 401, your API key is missing or invalid.

## Rate Limits

There is no hard rate limit currently. We recommend staying under **100 requests/minute per API key** for optimal performance. Burst traffic is acceptable. If you need sustained high throughput, contact us.

---

## Addresses

### GET /api/addresses

List all forwarding addresses for the authenticated user.

**Response:**
```json
{
  "addresses": [
    {
      "id": "01J...",
      "local_part": "invoices",
      "webhook_url": "https://example.com/webhook",
      "webhook_format": "json",
      "webhook_method": "POST",
      "attachment_mode": "url",
      "is_active": 1,
      "simulation": 0,
      "store_attachments": 1,
      "store_content": 1,
      "created_at": "2026-04-29T10:00:00Z",
      "updated_at": "2026-04-29T10:00:00Z"
    }
  ]
}
```

### POST /api/addresses

Create a new forwarding address. Emails sent to `{local_part}@mx-forwarder.com` will be forwarded to the webhook URL.

> **Important:** MX Forwarder validates your webhook URL with a HEAD request before saving. Your endpoint must return a 2xx status code for the address to be created. Add a HEAD handler to your webhook endpoint if you don't have one.

**Request Body (JSON):**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| local_part | string | No | Local part of the email (e.g. "invoices"). Random if omitted. |
| webhook_url | string | No | URL to forward emails to. Must be HTTPS and return 2xx on HEAD. |
| webhook_format | "json" \| "raw" | No | Payload format (default: "json") |
| webhook_method | "POST" \| "GET" \| "PUT" \| "PATCH" | No | HTTP method (default: "POST") |
| attachment_mode | "url" \| "inline" | No | "url" for signed R2 links, "inline" for base64 |
| simulation | boolean | No | Enable test mode (no actual forwarding) |
| store_attachments | boolean | No | Store attachments in R2 (default: true) |
| store_content | boolean | No | Retain email body/raw after delivery (default: true for regular users, false for reseller sub-accounts) |

**Response (201):**
```json
{
  "address": {
    "id": "01J...",
    "local_part": "invoices",
    "email": "invoices@mx-forwarder.com"
  },
  "webhook_secret": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
```

> The `webhook_secret` is shown only once. Store it securely — you need it to verify webhook signatures.

### GET /api/addresses/{id}

Get a single address by ID.

**Response:**
```json
{
  "address": {
    "id": "01J...",
    "local_part": "invoices",
    "webhook_url": "https://example.com/webhook",
    "webhook_format": "json",
    "webhook_method": "POST",
    "attachment_mode": "url",
    "is_active": 1,
    "simulation": 0,
    "store_attachments": 1,
    "store_content": 1,
    "created_at": "2026-04-29T10:00:00Z",
    "updated_at": "2026-04-29T10:00:00Z"
  }
}
```

### PUT /api/addresses/{id}

Update an existing address configuration. All fields are optional — only provided fields are updated.

**Request Body (JSON):**

| Field | Type | Description |
|-------|------|-------------|
| webhook_url | string | New webhook URL (must be HTTPS, validated with HEAD) |
| webhook_format | "json" \| "raw" | Payload format |
| webhook_method | "POST" \| "GET" \| "PUT" \| "PATCH" | HTTP method |
| attachment_mode | "url" \| "inline" | Attachment delivery mode |
| simulation | boolean | Test mode (emails stored, not forwarded) |
| store_attachments | boolean | Store attachments in R2 |
| store_content | boolean | Retain email body/raw after delivery |
| is_active | boolean | Enable/disable the address |

**Response:**
```json
{ "success": true }
```

### DELETE /api/addresses/{id}

Permanently delete an address and all associated emails, attachments, and delivery logs.

**Response:**
```json
{ "success": true }
```

---

## Emails

### GET /api/emails

List received emails with cursor-based pagination and optional filtering.

**Query Parameters:**

| Param | Type | Description |
|-------|------|-------------|
| q | string | Full-text search query (searches subject, sender, body) |
| address_id | string | Filter by address ID |
| limit | number | Results per page (default: 50, max: 100) |
| cursor | string | Pagination cursor from previous response |

**Response:**
```json
{
  "emails": [
    {
      "id": "01J...",
      "address_id": "01J...",
      "message_id": "<abc@example.com>",
      "envelope_from": "sender@example.com",
      "envelope_to": "invoices@mx-forwarder.com",
      "from_addr": "sender@example.com",
      "to_addr": "invoices@mx-forwarder.com",
      "subject": "Invoice #1234",
      "size_bytes": 12345,
      "spf_result": "pass",
      "dkim_result": "pass",
      "dmarc_result": "pass",
      "received_at": "2026-04-29T10:00:00Z"
    }
  ],
  "next_cursor": "2026-04-29T09:00:00Z"
}
```

> `next_cursor` is `null` when there are no more results.

### GET /api/emails/{id}

Get full email details including attachments and delivery history.

**Response:**
```json
{
  "email": {
    "id": "01J...",
    "address_id": "01J...",
    "envelope_from": "sender@example.com",
    "envelope_to": "invoices@mx-forwarder.com",
    "from_addr": "sender@example.com",
    "to_addr": "invoices@mx-forwarder.com",
    "subject": "Invoice #1234",
    "text_body": "Please find attached...",
    "html_body": "<html>...</html>",
    "headers_json": "{...}",
    "size_bytes": 12345,
    "spf_result": "pass",
    "dkim_result": "pass",
    "dmarc_result": "pass",
    "received_at": "2026-04-29T10:00:00Z"
  },
  "attachments": [
    {
      "id": "01J...",
      "file_name": "invoice.pdf",
      "content_type": "application/pdf",
      "size_bytes": 54321
    }
  ],
  "deliveries": [
    {
      "id": "01J...",
      "status": "delivered",
      "http_status": 200,
      "attempt_count": 1,
      "last_attempted_at": "2026-04-29T10:00:01Z"
    }
  ]
}
```

### GET /api/emails/{id}/raw

Download the original RFC822 MIME message as an .eml file.

**Response:** Binary file with `Content-Type: message/rfc822`

### POST /api/emails/{id}/retry

Re-queue the email for webhook delivery. Useful for retrying failed deliveries.

**Response:**
```json
{ "success": true }
```

---

## Deliveries

### GET /api/deliveries

List webhook delivery logs with optional filtering.

**Query Parameters:**

| Param | Type | Description |
|-------|------|-------------|
| address_id | string | Filter by address ID |
| email_id | string | Filter by email ID |
| status | "pending" \| "delivered" \| "failed" | Filter by delivery status |
| limit | number | Results per page (default: 50, max: 100) |
| cursor | string | Pagination cursor |

**Response:**
```json
{
  "deliveries": [
    {
      "id": "01J...",
      "email_id": "01J...",
      "address_id": "01J...",
      "status": "delivered",
      "http_status": 200,
      "response_body": "OK",
      "attempt_count": 1,
      "error_message": null,
      "last_attempted_at": "2026-04-29T10:00:01Z"
    }
  ],
  "next_cursor": "2026-04-29T09:00:00Z"
}
```

---

## Usage

### GET /api/usage

Get current billing period usage stats and plan limits.

**Response:**
```json
{
  "plan": "starter",
  "month": "2026-04",
  "email_count": 142,
  "email_limit": 25000,
  "attachment_bytes": 10485760,
  "attachment_limit": 2147483648,
  "addresses_used": 3,
  "addresses_limit": 25
}
```

---

## Reseller

> Requires the `reseller` plan. See the [Reseller Integration Guide](#reseller-integration-guide) for a complete walkthrough.

Reseller endpoints let you manage sub-accounts and track aggregate usage. Sub-account email volume counts against the reseller's aggregate limit.

**Important — Sub-account email uniqueness:** The `email` field on sub-accounts is just an identifier — it doesn't need to be a real email address. Use a synthetic format like `feeder-{userId}@your-domain.com` to avoid conflicts. **Email uniqueness is scoped per reseller** (not global), so a sub-account email won't conflict with a direct MX Forwarder user who has the same email. Use `external_id` to store your own customer reference for easy mapping.

### GET /api/reseller/sub-accounts

List all sub-accounts managed by the authenticated reseller.

**Response:**
```json
{
  "sub_accounts": [
    {
      "id": "01J...",
      "email": "feeder-abc123@newsletter.acme.com",
      "plan": "reseller_sub",
      "external_id": "usr_abc123",
      "clerk_id": "",
      "created_at": "2026-04-29T10:00:00Z"
    }
  ],
  "total": 1
}
```

### POST /api/reseller/sub-accounts

Create a new sub-account. Returns an API key (shown only once).

**Request Body (JSON):**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| email | string | Yes | Identifier for the sub-account (can be synthetic) |
| plan | string | No | Plan for address/retention limits (default: "reseller_sub") |
| external_id | string | No | Your own customer ID for mapping |

**Response (201):**
```json
{
  "sub_account": {
    "id": "01J...",
    "email": "feeder-abc123@newsletter.acme.com",
    "plan": "reseller_sub",
    "external_id": "usr_abc123",
    "created_at": "2026-04-29T10:00:00Z"
  },
  "api_key": "mf_live_abc123def456..."
}
```

> Store the `api_key` — it is shown only once. Use it to make API calls on behalf of this sub-account.

### PATCH /api/reseller/sub-accounts/{id}

Update a sub-account's plan or external_id.

**Request Body (JSON):**

| Field | Type | Description |
|-------|------|-------------|
| plan | string | New plan for the sub-account |
| external_id | string | Update your customer reference |

**Response:**
```json
{ "sub_account": { "id": "01J...", "plan": "starter", "external_id": "usr_abc123" } }
```

### DELETE /api/reseller/sub-accounts/{id}

Delete a sub-account and all associated data (addresses, emails, attachments, delivery logs).

**Response:**
```json
{ "deleted": true }
```

### GET /api/reseller/sub-accounts/{id}/usage

Get monthly usage breakdown for a specific sub-account with optional date range.

**Query Parameters:**

| Param | Type | Description |
|-------|------|-------------|
| from | string | Start month (YYYY-MM) |
| to | string | End month (YYYY-MM) |

**Response:**
```json
{
  "sub_account": { "id": "01J...", "email": "feeder-abc@acme.com", "external_id": "usr_abc123" },
  "months": [
    { "month": "2026-04", "email_count": 1200, "attachment_bytes": 5000000 },
    { "month": "2026-03", "email_count": 800, "attachment_bytes": 3000000 }
  ],
  "total": { "email_count": 2000, "attachment_bytes": 8000000 }
}
```

### PUT /api/reseller/settings

Update org-level defaults for content retention. Controls whether new sub-account addresses retain email content after webhook delivery.

**Request Body (JSON):**

| Field | Type | Description |
|-------|------|-------------|
| store_content_default | boolean | Default content retention for new sub-account addresses |

**Response:**
```json
{ "success": true }
```

### GET /api/reseller/usage

Get aggregate usage across the reseller and all sub-accounts.

**Query Parameters:**

| Param | Type | Description |
|-------|------|-------------|
| month | string | Billing month in YYYY-MM format (default: current) |

**Response:**
```json
{
  "month": "2026-04",
  "aggregate": { "email_count": 15420, "attachment_bytes": 52428800 },
  "limits": { "emails_per_month": 1000000, "attachment_storage_bytes": 107374182400 },
  "sub_accounts": [
    { "user_id": "01J...", "email": "feeder-abc@acme.com", "email_count": 8200, "attachment_bytes": 30000000 }
  ]
}
```

### GET /api/reseller/sub-accounts/{id}/usage

Get monthly usage breakdown for a specific sub-account with optional date range.

**Query Parameters:**

| Param | Type | Description |
|-------|------|-------------|
| from | string | Start month (YYYY-MM) |
| to | string | End month (YYYY-MM) |

**Response:**
```json
{
  "sub_account": { "id": "01J...", "email": "feeder-abc@acme.com", "external_id": "usr_abc123" },
  "months": [
    { "month": "2026-04", "email_count": 1200, "attachment_bytes": 5000000 },
    { "month": "2026-03", "email_count": 800, "attachment_bytes": 3000000 }
  ],
  "total": { "email_count": 2000, "attachment_bytes": 8000000 }
}
```

---

## Webhooks

When an email is received, MX Forwarder POSTs a JSON payload to your webhook URL.

**Request Headers:**
- `X-MailForwarder-Signature`: HMAC-SHA256 hex signature of the request body
- `X-MailForwarder-Email-ID`: The email ID
- `X-MailForwarder-Delivery-ID`: The delivery attempt ID
- `Content-Type`: application/json

**Request Body:**
```json
{
  "id": "01JQWX...",
  "envelope": {
    "from": "sender@example.com",
    "to": "invoices@mx-forwarder.com"
  },
  "headers": {
    "subject": "Invoice #1234",
    "from": "Sender Name <sender@example.com>",
    "to": "invoices@mx-forwarder.com",
    "date": "Tue, 29 Apr 2026 10:00:00 +0000"
  },
  "plain": "Please find attached the invoice.",
  "html": "<html>...</html>",
  "attachments": [
    {
      "file_name": "invoice.pdf",
      "content_type": "application/pdf",
      "size": 12345,
      "url": "https://signed-r2-url..."
    }
  ],
  "spf": { "result": "pass" },
  "dkim": { "result": "pass" },
  "dmarc": { "result": "pass" }
}
```

> **Multi-tenant tip:** Use `envelope.to` to identify which address received the email. This is the field you should use to look up the associated user in your system.

---

## Webhook Signature Verification

Every webhook payload is signed with HMAC-SHA256 using the address's `webhook_secret`. Verify the signature to ensure the request came from MX Forwarder.

### Node.js example

```javascript
import crypto from "crypto";

function verifyWebhookSignature(body, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// In your webhook handler:
app.post("/webhook", (req, res) => {
  const signature = req.headers["x-mailforwarder-signature"];
  const isValid = verifyWebhookSignature(req.rawBody, signature, WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).send("Invalid signature");
  }

  const payload = JSON.parse(req.rawBody);
  // Process the email...
  res.status(200).send("OK");
});
```

### Python example

```python
import hmac, hashlib

def verify_signature(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature, expected)
```

---

## Pagination

List endpoints (`/api/emails`, `/api/deliveries`) use cursor-based pagination.

1. Make a request with an optional `limit` parameter (default: 50, max: 100)
2. The response includes `next_cursor`
3. Pass `next_cursor` as the `cursor` query parameter on the next request
4. When `next_cursor` is `null`, there are no more results

Example: `GET /api/emails?limit=20&cursor=2026-04-29T09:00:00Z`

---

## Error Handling

All API errors return a consistent JSON structure:

```json
{ "error": "Descriptive error message", "status": 401 }
```

| Code | Meaning |
|------|---------|
| 400 | Invalid request body or parameters |
| 401 | Unauthorized — missing or invalid API key |
| 403 | Forbidden — plan limit exceeded |
| 404 | Resource not found |
| 409 | Conflict — e.g. address local_part already taken, sub-account email already exists |
| 500 | Server error |

---

## MCP Server

MX Forwarder provides an MCP (Model Context Protocol) server for AI agent integration. All API operations are available as MCP tools.

### Local (stdio) — for Claude Desktop, Claude Code, Cursor

```json
{
  "mcpServers": {
    "mx-forwarder": {
      "command": "npx",
      "args": ["@mx-forwarder/mcp"],
      "env": { "MX_FORWARDER_API_KEY": "mf_live_your_api_key" }
    }
  }
}
```

### Remote (OAuth)

- MCP Endpoint: `https://mx-forwarder.com/api/mcp`
- OAuth Discovery: `https://mx-forwarder.com/.well-known/oauth-authorization-server`

### Available MCP Tools

| Tool | Description |
|------|-------------|
| list_addresses | List all forwarding addresses |
| get_address | Get address details |
| create_address | Create a new address |
| update_address | Update address configuration |
| delete_address | Delete an address |
| list_emails | List and search emails |
| get_email | Get email with attachments & deliveries |
| retry_email_delivery | Re-queue a failed delivery |
| list_deliveries | List webhook delivery logs |
| get_usage | Get usage stats and plan limits |
| list_sub_accounts | List reseller sub-accounts |
| create_sub_account | Create a new sub-account |
| update_sub_account | Update sub-account plan/external_id |
| delete_sub_account | Delete a sub-account |
| get_reseller_usage | Aggregate usage across sub-accounts |
| get_sub_account_usage | Per-sub-account usage with date range |

---

## Reseller Integration Guide

### Overview

The reseller plan lets you build email-to-webhook functionality into your own SaaS product. Create sub-accounts for your customers, track their usage, and get billed for aggregate consumption.

### Step-by-step

1. **Get a reseller account** — contact us to upgrade your plan
2. **Create sub-accounts** — `POST /api/reseller/sub-accounts` with a synthetic email and your `external_id`
3. **Store the API key** — each sub-account gets its own API key for programmatic access
4. **Create addresses** — use the sub-account's API key: `POST /api/addresses` with your webhook URL
5. **Receive emails** — emails to `{address}@mx-forwarder.com` are forwarded to your webhook. Use `envelope.to` to identify the recipient address.
6. **Monitor usage** — `GET /api/reseller/usage` for aggregate stats, `GET /api/reseller/sub-accounts/{id}/usage` for per-customer breakdowns

### Key points

- **Sub-account email** is just an identifier (e.g. `feeder-{userId}@your-domain.com`). It doesn't receive emails — the forwarding addresses do.
- **Email uniqueness** is scoped per reseller. No global conflicts.
- **Email volume** rolls up to the reseller's aggregate limit (1M emails/month).
- **Content retention** — by default, sub-account addresses don't store email body/attachments after webhook delivery. Enable `store_content` per address or set the org-level default via `PUT /api/reseller/settings`.
- **Sub-accounts can sign up** at mx-forwarder.com with the same email to get dashboard access (optional).

### Example: create a sub-account and address

```bash
# 1. Create sub-account (as reseller)
curl -X POST https://mx-forwarder.com/api/reseller/sub-accounts \
  -H "Authorization: Bearer mf_live_reseller_key" \
  -H "Content-Type: application/json" \
  -d '{"email": "feeder-user123@newsletter.acme.com", "external_id": "usr_123"}'
# Returns: { "sub_account": {...}, "api_key": "mf_live_sub_key..." }

# 2. Create address (as sub-account)
curl -X POST https://mx-forwarder.com/api/addresses \
  -H "Authorization: Bearer mf_live_sub_key" \
  -H "Content-Type: application/json" \
  -d '{"webhook_url": "https://your-app.com/webhooks/email"}'
# Returns: { "address": { "email": "mail-abc@mx-forwarder.com" }, "webhook_secret": "..." }

# 3. Monitor usage (as reseller)
curl https://mx-forwarder.com/api/reseller/sub-accounts/01J.../usage?from=2026-01&to=2026-04 \
  -H "Authorization: Bearer mf_live_reseller_key"
```