> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waniwani.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Email

> Send an email from a tool or a flow node with one call, and find every send in the email log.

<Note>
  **Platform feature.** Requires `WANIWANI_API_KEY`. Works whether your MCP server is self-hosted or on Managed Hosting. [About the Platform](/sdk/platform/overview).
</Note>

The email module sends one email from your agent to one person, usually the visitor it just talked with: a quote recap, a booking confirmation, a link to finish signing up. You write the email in your own project, the platform sends it, and every send lands in a log you can check.

## Setup

The email client sits on the Waniwani client instance:

```ts theme={null}
import { waniwani } from "@waniwani/sdk";

const wani = waniwani(); // reads WANIWANI_API_KEY from env
```

## Send an email

Render the email to HTML, then pass it with a recipient and a subject:

```tsx theme={null}
import { render } from "@react-email/components";
import { RecapEmail } from "./emails/recap";

const { id } = await wani.email.send({
  to: "visitor@example.com",
  subject: "Your quote",
  html: await render(<RecapEmail name="Sam" total="42.00" />),
});
```

Any HTML string works, and the SDK sends it exactly as you pass it. React Email is not a dependency of the SDK, so add it to your own project if you want it:

```bash theme={null}
bun add @react-email/components
```

Leave `text` out and a plain-text version is built from the HTML. Pass `text` when you want to write that version yourself.

## Send from a flow node or a tool

The request-scoped client carries the conversation's session id, so the log row points back to the conversation that sent the email. Where you get that client depends on how the tool is built.

Inside a flow node, it is the `waniwani` field of the node context:

```tsx theme={null}
import { render } from "@react-email/components";
import { RecapEmail } from "./emails/recap";

flow.addNode({
  id: "send_recap",
  run: async ({ state, waniwani }) => {
    const sent = await waniwani?.email.send({
      to: state.email,
      subject: "Your quote",
      html: await render(<RecapEmail name={state.name} total={state.total} />),
    });
    return { recapSent: sent !== undefined };
  },
});
```

Inside a tool registered with `server.registerTool`, get the scoped client with `extractScopedClient(extra)`:

```tsx theme={null}
import { extractScopedClient } from "@waniwani/sdk/mcp";
import { render } from "@react-email/components";
import { z } from "zod";
import { RecapEmail } from "./emails/recap";

server.registerTool(
  "send_recap",
  {
    title: "Send a recap email",
    description: "Send the visitor a recap of this conversation by email.",
    inputSchema: { email: z.string(), name: z.string(), total: z.string() },
  },
  async ({ email, name, total }, extra) => {
    const wani = extractScopedClient(extra);
    const sent = await wani?.email.send({
      to: email,
      subject: "Your quote",
      html: await render(<RecapEmail name={name} total={total} />),
    });
    return {
      content: [
        { type: "text", text: sent ? "Recap sent." : "Could not send the recap." },
      ],
    };
  },
);
```

Pass your own `sessionId` to override the one from the request.

## Who the email comes from

Every email leaves from a shared address carrying your agent's name, as it is set in the dashboard:

```
"Acme Assistant" <acme-assistant@notifications.waniwani.run>
```

Nobody receives mail at that address, so a visitor who replies gets a bounce. Set `replyTo` to a mailbox you read:

```ts theme={null}
await wani.email.send({ to, subject, html, replyTo: "support@example.com" });
```

## The email log

Every accepted send appears in the dashboard under **Settings > Modules > Email > Logs** with its recipient, subject and status. The status starts at sent and moves to delivered, bounced or complained as the receiving server answers. `send()` resolves with the id of that log row.

## Limits

|                      |                           |
| -------------------- | ------------------------- |
| Recipients           | One per call              |
| Subject              | 998 characters            |
| HTML                 | 512 KB                    |
| Plain text           | 512 KB                    |
| Session id           | 2 KB                      |
| `send()` calls       | 30 per minute per API key |
| Attachments, cc, bcc | Not supported             |

## Errors

Without an API key, `send()` throws before making any request. Every refusal from the platform throws `WaniWaniError`: its `status` is the HTTP status and its message starts with the code.

| Status | Code                        | Meaning                                                                                                                                                                 |
| ------ | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `VALIDATION_ERROR`          | The recipient or `replyTo` is not an email address, the subject or a body is empty or outside the limits above, or the subject or `sessionId` contains a NUL character. |
| 400    | `ENVIRONMENT_HAS_NO_AGENT`  | The API key belongs to an environment with no agent, so there is no name to send as.                                                                                    |
| 401    | `INVALID_API_KEY`           | The API key is unknown, for example a key from another region than `WANIWANI_API_URL`.                                                                                  |
| 403    | `API_KEY_REVOKED`           | The API key was revoked in the dashboard.                                                                                                                               |
| 404    | `MCP_PROJECT_NOT_FOUND`     | The agent behind the API key no longer exists.                                                                                                                          |
| 422    | `EMAIL_REJECTED`            | The email provider refused the message as written. The rest of the message says why.                                                                                    |
| 429    | none                        | This API key made too many `send()` calls in the last minute. The message is a sentence asking you to wait.                                                             |
| 500    | `INTERNAL_ERROR`            | Something failed on our side, possibly after the email went out. Retrying can send it twice.                                                                            |
| 502    | `EMAIL_SEND_FAILED`         | The email provider failed or is rate limiting. No log row was written.                                                                                                  |
| 503    | `DATABASE_CONNECTION_ERROR` | Same as 500, caused by a lost database connection.                                                                                                                      |

```ts theme={null}
import { WaniWaniError } from "@waniwani/sdk";

try {
  await wani.email.send({ to, subject, html });
} catch (error) {
  if (error instanceof WaniWaniError && error.status === 422) {
    // the message itself needs fixing, retrying will not help
  }
  throw error;
}
```
