# mailer123 — Integration Guide for AI Coding Agents

You are an AI assistant (Claude Code, Cursor, Windsurf, Copilot, etc.) helping a
developer add **mailer123** — an email‑sending platform — to a **React, Node, or
Next.js** app. This guide is everything you need: the SDK, the REST API, the MCP
option, security rules, and copy‑paste patterns. Follow it exactly.

There are three ways to integrate. Pick by the task:

| Path | Use when |
| --- | --- |
| **Node SDK** (`mailer123`) | Node / Next.js / edge apps sending email. Simplest, typed. |
| **REST API** (`/api/v1`) | Any language, or features the SDK doesn't wrap yet (contacts, campaigns, templates). |
| **MCP server** | You want an AI assistant to operate the product directly (not app code). |

---

## 0. Golden rules (read first)

1. **The API key is a server‑side secret.** It starts with `m123_`. **Never** put
   it in React/browser/client code, a `NEXT_PUBLIC_*` var, a mobile app, or a Git
   commit. Anyone with the key can send mail as the account.
2. **Call mailer123 from a backend**, never from the browser. In React/Next.js
   that means a **route handler**, **server action**, or your own API server. The
   browser calls *your* endpoint; *your* endpoint calls mailer123.
3. **Store the key in an env var** (`MAILER123_API_KEY`) and load it server‑side
   only. Add it to `.env.local` / your host's secret manager, and to
   `.gitignore`.
4. **Verify a domain first.** You can only send from an address on a domain the
   workspace has verified (in the dashboard under **Domains**).
5. **Handle failures.** Every call can fail (quota, suppression, unverified
   sender, rate limit). Branch on the stable error `code`, don't assume success.

---

## 1. Get an API key

In the mailer123 dashboard: **Settings → API → Create key**. The key is shown
**once** — copy it into your server env. Keys have a **scope**:

- `sending` — can send email only (`/v1/emails`). Use this for an app that just
  fires transactional mail; it can't read your audience.
- `full` — everything: email **plus** contacts, campaigns, and templates.

Pick the narrowest scope that fits.

A key is also scoped to one or more **projects** (workspaces): either **all**
projects you own, or a chosen subset. Each request acts on one project — see
"Choosing a project" below.

---

## 2. The Node SDK (`mailer123`)

A small, typed, dependency‑free client. Works anywhere `fetch` exists — Node 18+,
Next.js route handlers/server actions, edge runtimes. **It currently covers
sending email**; for contacts/campaigns/templates use the REST API (section 3).

### Install

```bash
npm install mailer123      # or: pnpm add mailer123 / yarn add mailer123
```

### Initialize

```ts
import { Mailer123 } from "mailer123";

// Reads MAILER123_API_KEY from the environment (recommended):
const mailer = new Mailer123();

// …or pass it explicitly:
const mailer = new Mailer123(process.env.MAILER123_API_KEY!);
```

Constructor: `new Mailer123(apiKey?, options?)`

| Arg | Meaning |
| --- | --- |
| `apiKey` | Your `m123_…` key. Falls back to `MAILER123_API_KEY`. |
| `options.baseUrl` | API base URL. Defaults to `https://mailer123.com` (or `MAILER123_BASE_URL`). |
| `options.fetch` | Custom `fetch` (only needed on Node < 18). |

### Send an email

```ts
const { data, error } = await mailer.emails.send({
  from: "you@yourdomain.com",          // optional; must be a verified domain
  to: "customer@example.com",
  subject: "Your receipt",
  html: "<p>Thanks for your order, {{name|there}}!</p>",
  text: "Thanks for your order!",      // optional plain-text alternative
  replyTo: "support@yourdomain.com",   // optional
  unsubscribeTemplate: "minimal",      // "none" | "minimal" | "branded" | "card"
  attachments: [                        // optional
    { filename: "receipt.pdf", content: "<base64>" },                  // inline bytes
    { filename: "logo.png", path: "https://cdn.example.com/logo.png" }, // or a URL
  ],
});

if (error) {
  // never throws — branch on the stable code
  console.error(error.code, error.message); // e.g. "quota_exceeded"
} else {
  console.log("Sent:", data.id); // provider message id
}
```

**The SDK never throws on an API error.** Every call resolves to
`{ data, error }` — exactly one is set. `data` is `{ id }` on success; `error` is
`{ message, code, status }` on failure (see codes in section 6).

---

## 3. The REST API (`/api/v1`)

Use this from any language, or for resources the SDK doesn't wrap yet. Base URL:
`https://mailer123.com/api/v1`. Authenticate every request with the key as a
bearer token. Responses are JSON; errors use the envelope
`{ "error": { "message", "code" } }`.

```
Authorization: Bearer m123_your_api_key
Content-Type: application/json
```

### Endpoints

| Method & path | Scope | Purpose |
| --- | --- | --- |
| `POST /v1/emails` | sending | Send one transactional email. Returns `{ id }` (202). |
| `GET /v1/contacts` | full | List contacts → `{ data: [...] }`. |
| `POST /v1/contacts` | full | Create a contact: `{ email, name?, custom_fields? }`. |
| `GET /v1/contacts/{id}` | full | Retrieve one contact. |
| `PATCH /v1/contacts/{id}` | full | Update a contact. |
| `DELETE /v1/contacts/{id}` | full | Delete a contact. |
| `GET /v1/groups` | full | List groups → `{ data: [...] }`. |
| `POST /v1/groups` | full | Create a group: `{ name, contact_ids?, emails? }` (needs ≥1 resolvable member). |
| `GET /v1/groups/{id}` | full | Retrieve one group with members. |
| `PATCH /v1/groups/{id}` | full | Update a group: `{ name?, contact_ids? }` (contact_ids replaces members). |
| `POST /v1/groups/{id}/contacts` | full | Add members: `{ contact_ids?, emails? }` (no duplicates). |
| `DELETE /v1/groups/{id}` | full | Delete a group (contacts kept). |
| `GET /v1/campaigns` | full | List campaigns + engagement. |
| `POST /v1/campaigns/{id}/send` | full | Start sending a draft/scheduled campaign (202). |
| `GET /v1/templates` | full | List templates. |
| `GET /v1/templates/{id}` | full | Retrieve a template. |
| `GET /v1/projects` | sending | List the projects this key can act on → `{ data: [{ id, name, suspended }] }`. |
| `GET /v1/projects/{id}/domains` | full | List a project's sending domains + verification status (read-only). |
| `GET /v1/projects/{id}/domains/{domain}` | full | Retrieve one domain with DNS records + recommendations (read-only). |

### Choosing a project

Each request acts on a single project. If your key covers exactly **one**
project, it's used automatically — do nothing. If it covers **several**, name the
target with the `X-Project-Id` header (its value is a project `id` from
`GET /v1/projects`):

```
Authorization: Bearer m123_your_api_key
X-Project-Id: 665f0c2a9b1e4d3a2c1f0e9d
```

Omitting the header on a multi-project key returns `400 validation_error`;
passing a project the key isn't authorized for returns `403 forbidden`.

The `/v1/emails` body is snake_case: `from?`, `to`, `subject`, `html`, `text?`,
`reply_to?`, `unsubscribe_template?`, `attachments?` (`{ filename, content? |
path?, content_type? }`).

### Example — send via fetch

```ts
const res = await fetch("https://mailer123.com/api/v1/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.MAILER123_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "you@yourdomain.com",
    to: "customer@example.com",
    subject: "Hello",
    html: "<p>Hi {{name|there}}</p>",
  }),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error?.message); // { error: { message, code } }
console.log(json.id);
```

### Example — create a contact

```ts
await fetch("https://mailer123.com/api/v1/contacts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.MAILER123_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    email: "jane@example.com",
    name: "Jane",
    custom_fields: { company: "Acme", plan: "pro" },
  }),
});
```

---

## 4. Framework patterns (copy‑paste)

### Next.js (App Router) — route handler

```ts
// app/api/send/route.ts  — runs on the SERVER only
import { Mailer123 } from "mailer123";

const mailer = new Mailer123(); // reads MAILER123_API_KEY

export async function POST(request: Request) {
  const { to, subject, html } = await request.json();
  const { data, error } = await mailer.emails.send({
    from: "you@yourdomain.com",
    to,
    subject,
    html,
  });
  if (error) {
    return Response.json({ error: error.message }, { status: error.status || 502 });
  }
  return Response.json({ id: data.id }, { status: 202 });
}
```

### Next.js — server action

```ts
"use server";
import { Mailer123 } from "mailer123";

const mailer = new Mailer123();

export async function sendWelcome(email: string) {
  const { error } = await mailer.emails.send({
    from: "you@yourdomain.com",
    to: email,
    subject: "Welcome!",
    html: "<p>Glad you're here, {{name|there}}.</p>",
  });
  if (error) throw new Error(error.message);
}
```

### Express / Node backend

```ts
import express from "express";
import { Mailer123 } from "mailer123";

const mailer = new Mailer123();
const app = express();
app.use(express.json());

app.post("/api/send", async (req, res) => {
  const { data, error } = await mailer.emails.send({
    from: "you@yourdomain.com",
    to: req.body.to,
    subject: req.body.subject,
    html: req.body.html,
  });
  if (error) return res.status(error.status || 502).json({ error: error.message });
  res.status(202).json({ id: data.id });
});
```

### React (frontend) — call your backend, NOT mailer123

```tsx
// The browser never sees the API key. It calls YOUR endpoint (above).
async function handleSubmit(email: string) {
  const res = await fetch("/api/send", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ to: email, subject: "Hi", html: "<p>Hello</p>" }),
  });
  if (!res.ok) {
    const { error } = await res.json();
    // show error to the user
  }
}
```

> If you ever feel tempted to call `mailer123` directly from a React component or
> put the key in `NEXT_PUBLIC_*`, **stop** — that leaks the key to every visitor.
> Always proxy through a server route.

---

## 5. Personalization (merge variables)

Subjects and HTML bodies support `{{...}}` tokens, filled per recipient at send
time:

- `{{name}}` — the contact's name (resolved from the matching contact)
- `{{email}}` — the recipient address
- `{{unsubscribe_url}}` — a working one‑click unsubscribe link
- `{{your_custom_field}}` — any custom field key on the contact
- `{{name|fallback}}` — supply a default after `|` for when the value is empty

Unknown tokens render empty. To make `{{name}}`/custom fields resolve, the
recipient should exist as a contact (create them via `/v1/contacts`).

---

## 6. Errors, limits & reliability

### Result / error shape

SDK calls resolve to `{ data, error }`; the REST API returns
`{ "error": { "message", "code" } }` with an HTTP status. Branch on `code`:

| `code` | Meaning | What to do |
| --- | --- | --- |
| `unauthorized` | Missing/invalid/revoked key | check `MAILER123_API_KEY` |
| `insufficient_scope` | `sending` key on a `full` endpoint | use a full‑scope key |
| `forbidden` | Sender not verified / domain send‑blocked | use a verified `from` |
| `validation_error` | Bad request body | fix the field named in `message` |
| `quota_exceeded` | Daily/monthly send cap hit | back off; raise plan |
| `rate_limit_exceeded` | >~10 req/s for the key | retry after `Retry-After` |
| `conflict` | Recipient suppressed / duplicate | don't re‑send; remove from list |
| `not_found` | Unknown id | re‑list to get a valid id |
| `send_failed` / `internal_error` | Provider/server error | retry with backoff |
| `network_error` *(SDK only)* | Request never reached the API | retry with backoff |

### Limits

- **Rate limit:** ~10 requests/second per key. On `429`, honor the `Retry-After`
  header and retry with exponential backoff. Don't fan out unbounded loops.
- **Send quota:** each plan has daily and monthly caps, counted **per
  recipient**. A `quota_exceeded` error means you've hit it.
- **Suppression:** addresses that bounced, complained, or unsubscribed are
  rejected automatically. Never try to work around it.
- **Verified senders:** `from` must be on a verified domain, or omit it for the
  project default.

### Reliability tips

- Sends are **synchronous** and return a message id — log it for support.
- The same email sent twice = two emails; if you need idempotency, dedupe on your
  side (e.g. a per‑event key before calling send).
- Put bulk sends behind your own job/queue, and respect the rate limit.

---

## 7. MCP — let an AI operate the product

If the goal isn't app code but having an AI assistant **drive** mailer123
(send mail, manage contacts/campaigns/templates, read analytics in natural
language), connect it to the **MCP server** instead:

- Endpoint: `https://mailer123.com/api/mcp` (streamable HTTP).
- Auth: the same `Bearer m123_…` key. `sending` keys get the email tools; `full`
  keys get everything.
- Tools (by area): **projects** (`list_projects`), **domains** (`list_domains`,
  `get_domain` — read-only), **email** (`send_email`, `send_test_email`),
  **contacts**, **groups**, **templates**, **campaigns** (create/send/schedule/…),
  **analytics** (`get_metrics`), and `read_docs`.
- Projects: every tool (except `list_projects`/`read_docs`) takes an optional
  `project_id`. Omit it when the key covers a single project; when it covers
  several, call `list_projects` and pass the chosen `id`.
- Setup per client and the full tool reference: see the MCP docs at
  `https://mailer123.com/docs/mcp`. A connected assistant can also call
  `read_docs` for live guidance.

---

## 8. Checklist for the agent

When wiring mailer123 into a user's app, make sure you:

- [ ] Put the key in a **server‑side env var**; never expose it to the client.
- [ ] Call the SDK/REST from a **backend route/action**, not the browser.
- [ ] Use a **verified `from`** (or omit it).
- [ ] **Check `error`** on every call and surface a useful message.
- [ ] Add **retry/backoff** for `rate_limit_exceeded` / `network_error`.
- [ ] Include an `{{unsubscribe_url}}` (or an unsubscribe footer) in marketing mail.
- [ ] Use the narrowest **scope** (`sending` vs `full`) the app needs.

---

Full docs: `https://mailer123.com/docs` · API reference:
`https://mailer123.com/api-reference` · MCP: `https://mailer123.com/docs/mcp`
