Sending email
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#
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 -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>" }'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);{ "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}.
Body parameters#
The HTTP API uses snake_case. The SDK takes the same fields in camelCase (replyTo, scheduledAt, templateId).
- fromstringRequired
you@yourdomain.comorYour Name <you@yourdomain.com>. The domain must be verified in your workspace (subdomains of a verified domain work too).- tostring | string[]Required
- Up to 50 addresses across to, cc and bcc.
- subjectstringRequired
- Optional when the template has a default subject.
- htmlstring
- The HTML body. Required unless you pass text or a template.
- textstring
- The plain-text body. The SDK renders one from react when you leave it out.
- reactReactElement
- SDK only. Rendered to html and text with @react-email/render.
- ccstring | string[]
- Carbon copy.
- bccstring | string[]
- Blind carbon copy.
- reply_tostring | string[]
- Up to 10 Reply-To addresses.
- template_idstring
- A saved template's id or external id. See Templates.
- variablesobject
- Values for the template's {{merge_tags}}.
- scheduled_atstring
- 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.
- headersobject
- 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. Attachments aren't supported: link to the file instead.
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:
import { render, toPlainText } from "@react-email/render";
const html = await render(<WelcomeEmail name="Sam" />);const text = toPlainText(html);The from address#
Mailmanny sends only from domains your workspace has verified in 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 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.
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 -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#
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#
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.
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 ordercurl -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#
Full-access keys can look up any email sent through the API. Sending keys see the emails they sent themselves.
const { data } = await mm.emails.get("0199f2a4-5b1c-7d3e-9a2b-4c5d6e7f8a9b");{ "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.