# Sending email

> Send transactional email with React Email, HTML or a saved template. Batch, schedule, tag and track it.

Source: https://www.mailmanny.com/docs/sending
All docs for AI agents: https://www.mailmanny.com/llms.txt

Send one-to-one email from your app: receipts, password resets, order updates. Write it in React Email, pass HTML, or reuse a template you pushed. The API is compatible with Resend's.

## Send an email

`POST /api/v1/emails` (permission: `emails:send`)

**Node.js SDK**

```tsx
import { Mailmanny } from "mailmanny";
import WelcomeEmail from "./emails/welcome";

const mm = new Mailmanny(process.env.MAILMANNY_API_KEY);

const { data, error } = await mm.emails.send({
  from: "Manny's Bakery <hello@mannysbakery.com>",
  to: "sam@example.com",
  subject: "Welcome to the bakery!",
  react: <WelcomeEmail name="Sam" />,
});

if (error) {
  console.error(error.name, error.message);
} else {
  console.log("Queued", data.id);
}
```

**curl**

```bash
curl -X POST https://www.mailmanny.com/api/v1/emails \
  -H "Authorization: Bearer $MAILMANNY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Manny <hello@mannysbakery.com>",
    "to": "sam@example.com",
    "subject": "Welcome to the bakery!",
    "html": "<p>Your first croissant is on us.</p>"
  }'
```

**fetch**

```tsx
import { render } from "@react-email/render";
import WelcomeEmail from "./emails/welcome";

const res = await fetch("https://www.mailmanny.com/api/v1/emails", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.MAILMANNY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "Manny <hello@mannysbakery.com>",
    to: "sam@example.com",
    subject: "Welcome to the bakery!",
    html: await render(<WelcomeEmail name="Sam" />),
  }),
});

const body = await res.json();
if (!res.ok) throw new Error(`${body.name}: ${body.message}`);
console.log("Queued", body.id);
```

**200 OK**

```json
{
  "id": "0199f2a4-5b1c-7d3e-9a2b-4c5d6e7f8a9b",
  "ids": ["0199f2a4-5b1c-7d3e-9a2b-4c5d6e7f8a9b"]
}
```

Mailmanny creates one message per `to` address and returns all their ids in `ids`; `id` is the first one. The email is queued, not sent yet: delivery takes seconds, and you can follow it with [GET /emails/{id}](https://www.mailmanny.com/docs/sending.md#retrieve).

### Body parameters

The HTTP API uses snake_case. The SDK takes the same fields in camelCase (`replyTo`, `scheduledAt`, `templateId`).

- `from` (string, required): `you@yourdomain.com` or `Your Name <you@yourdomain.com>`. The domain must be verified in your workspace (subdomains of a verified domain work too).
- `to` (string | string[], required): Up to 50 addresses across to, cc and bcc.
- `subject` (string, required): Optional when the template has a default subject.
- `html` (string): The HTML body. Required unless you pass text or a template.
- `text` (string): The plain-text body. The SDK renders one from react when you leave it out.
- `react` (ReactElement): SDK only. Rendered to html and text with @react-email/render.
- `cc` (string | string[]): Carbon copy.
- `bcc` (string | string[]): Blind carbon copy.
- `reply_to` (string | string[]): Up to 10 Reply-To addresses.
- `template_id` (string): A saved template's id or external id. See Templates.
- `variables` (object): Values for the template's {{merge_tags}}.
- `scheduled_at` (string): ISO 8601 with a time zone, or natural language like "in 10 minutes". Up to 30 days ahead.
- `tags` ({ name, value }[]): Up to 20 labels for your own reporting. Letters, numbers, underscores and dashes.
- `headers` (object): Extra headers, up to 50. From, To, Subject, Reply-To, List-Unsubscribe and MIME headers are set by Mailmanny.

Send an `Idempotency-Key` header to retry safely; see [Idempotency](https://www.mailmanny.com/docs/idempotency.md). Attachments aren't supported: link to the file instead.

**Everything at once**

```tsx
const { data, error } = await mm.emails.send(
  {
    from: "Manny's Bakery <orders@mannysbakery.com>",
    to: ["sam@example.com"],
    cc: "accounts@example.com",
    replyTo: "help@mannysbakery.com",
    subject: "Your order #1042 is ready",
    react: <OrderReadyEmail orderId="1042" />,
    tags: [{ name: "category", value: "order_ready" }],
    headers: { "X-Entity-Ref-ID": "order-1042" },
  },
  { idempotencyKey: "order-ready-1042" },
);
```

## React Email

Pass any React element as `react` and the SDK renders it on your server with `@react-email/render`, the exact way Resend's SDK does. It also renders a plain-text version, which helps your mail land in the inbox. If you call the HTTP API yourself, render first and send `html`:

```tsx
import { render, toPlainText } from "@react-email/render";

const html = await render(<WelcomeEmail name="Sam" />);
const text = toPlainText(html);
```

> **Note:** Install @react-email/render next to mailmanny
> It's an optional peer dependency. Without it, sending a `react` email returns an error named `missing_dependency` with the command to fix it.

## The from address

Mailmanny sends only from domains your workspace has verified in [Settings → Domains](https://www.mailmanny.com/settings/domains). Anything else fails with 422 `invalid_from_address`. Free mailbox domains like gmail.com can't be verified by anyone, so use your own domain and set `reply_to` if replies should go elsewhere.

Mailmanny never sends to disposable (throwaway) inboxes. If `to`, `cc` or `bcc` includes one, the request fails with 422 `validation_error` and `details.disposable_recipients` lists them. Nothing is sent, so remove them and send again.

## Send a saved template

Templates you build in the editor or push with the [CLI](https://www.mailmanny.com/docs/templates.md) can be sent by id or external id. Mailmanny fills in `{{merge_tags}}` from `variables` (a fallback after a pipe works too: `{{first_name|there}}`). The template's subject is used unless you pass one.

**Node.js SDK**

```ts
await mm.emails.send({
  from: "Manny's Bakery <hello@mannysbakery.com>",
  to: "sam@example.com",
  templateId: "welcome", // the template's id or external id
  variables: { first_name: "Sam", coupon: "CROISSANT" },
});
```

**curl**

```bash
curl -X POST https://www.mailmanny.com/api/v1/emails \
  -H "Authorization: Bearer $MAILMANNY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Manny <hello@mannysbakery.com>",
    "to": "sam@example.com",
    "template_id": "welcome",
    "variables": { "first_name": "Sam", "coupon": "CROISSANT" }
  }'
```

## Schedule for later

```ts
await mm.emails.send({
  from: "Manny <hello@mannysbakery.com>",
  to: "sam@example.com",
  subject: "Your trial ends tomorrow",
  html: "<p>Keep your bread coming…</p>",
  scheduledAt: "in 2 hours", // or new Date("2026-10-20T09:00:00Z"), or an ISO 8601 string
});
```

Scheduled emails show `last_event: "scheduled"` until they go out. Times need a zone (`Z` or an offset like `+02:00`) so there's no guessing.

## Send a batch

`POST /api/v1/emails/batch` (permission: `emails:send`)

Up to 100 emails in one request, in Resend's format: an array in, `{ data: [{ id, ids }] }` out, in the same order. Mailmanny validates every email before queueing any, so a typo in email 37 doesn't send the first 36. If something fails while queueing, the error says which email (`details.index`) and which were already queued (`details.queued`). Retry with the same `Idempotency-Key` to finish the rest without duplicates.

**Node.js SDK**

```tsx
const { data, error } = await mm.emails.batch([
  { from: "Manny <hello@mannysbakery.com>", to: "sam@example.com", subject: "Your receipt", react: <Receipt id="1" /> },
  { from: "Manny <hello@mannysbakery.com>", to: "ana@example.com", subject: "Your receipt", react: <Receipt id="2" /> },
]);

// data.data → [{ id, ids }, { id, ids }] in the same order
```

**curl**

```bash
curl -X POST https://www.mailmanny.com/api/v1/emails/batch \
  -H "Authorization: Bearer $MAILMANNY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '[
    { "from": "Manny <hello@mannysbakery.com>", "to": "sam@example.com", "subject": "Hi Sam", "html": "<p>Hi</p>" },
    { "from": "Manny <hello@mannysbakery.com>", "to": "ana@example.com", "subject": "Hi Ana", "html": "<p>Hi</p>" }
  ]'
```

## Check what happened

`GET /api/v1/emails/{id}` (permission: `emails:send`)

Full-access keys can look up any email sent through the API. Sending keys see the emails they sent themselves.

```ts
const { data } = await mm.emails.get("0199f2a4-5b1c-7d3e-9a2b-4c5d6e7f8a9b");
```

**200 OK**

```json
{
  "object": "email",
  "id": "0199f2a4-5b1c-7d3e-9a2b-4c5d6e7f8a9b",
  "from": "Manny's Bakery <orders@mannysbakery.com>",
  "to": ["sam@example.com"],
  "reply_to": ["help@mannysbakery.com"],
  "subject": "Your order #1042 is ready",
  "status": "delivered",
  "last_event": "opened",
  "tags": [{ "name": "category", "value": "order_ready" }],
  "created_at": "2026-10-15T09:00:00.000Z",
  "scheduled_at": null,
  "sent_at": "2026-10-15T09:00:02.114Z",
  "delivered_at": "2026-10-15T09:00:03.950Z",
  "opened_at": "2026-10-15T09:12:41.003Z",
  "clicked_at": null,
  "bounced_at": null,
  "complained_at": null,
  "error": null
}
```

- `scheduled`: Waiting for scheduled_at.
- `queued`: In the send queue. Usually seconds.
- `delivery_delayed`: Held back by a sending cap or the daily quota, or waiting for a quick automatic review (see Compliance). It goes out automatically when capacity frees up or the review approves it.
- `sent`: Handed to the mail servers.
- `delivered`: The recipient's server accepted it.
- `opened / clicked`: Opened, or a link was clicked.
- `bounced`: Rejected by the recipient's server. Hard bounces are suppressed for good.
- `complained`: Marked as spam. The address is suppressed.
- `suppressed`: Not sent: the address previously bounced, complained or is otherwise blocked.
- `failed`: Couldn't be sent; error says why.
- `canceled`: Canceled before it was sent.

## Limits and deliverability

- Each workspace has daily and hourly sending caps that grow as your bounce and complaint rates stay low. When you reach one, emails wait (`delivery_delayed`) instead of failing.
- Addresses that hard-bounced or marked you as spam are never emailed again, from the API or anywhere else.
- The API is for transactional mail: things a person asked for or needs, like receipts, password resets and alerts. Newsletters and promotions go through campaigns and automations, which add the unsubscribe link and your address for you. See [Compliance requirements](https://www.mailmanny.com/docs/compliance.md).
