Skip to content
Guides

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#

POST/api/v1/emailsPermission: emails:send
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);}
Response200 OKJSON
{  "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.com or Your 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.

Everything at onceTSX
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);

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" },});

Schedule for later#

TypeScript
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/batchPermission: 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.

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

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.

TypeScript
const { data } = await mm.emails.get("0199f2a4-5b1c-7d3e-9a2b-4c5d6e7f8a9b");
Response200 OKJSON
{  "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.