# Migrate from Resend to MailMoose

> Move your app's transactional email, such as password resets and receipts, from Resend to MailMoose: what to set up, what changes in your code, what is not available, and when to stay on Resend.

Source: https://www.mailmoose.dev/docs/integrations/migrate-from-resend
Last updated: 2026-11-05

MailMoose has a transactional email API your app can send through, so password resets, receipts and welcome emails can move from Resend with a new URL, a new key and a few small changes. They then sit next to your inboxes, on unlimited domains with the paid plan, and mail you receive does not count against what you send.

Using a coding agent? Give it this page. Everything it needs for the move is here and in [Using the API](https://www.mailmoose.dev/docs/integrations/using-the-api.md). If it can't open web pages, press **Copy page** at the top of each guide and paste the text in.

## When to stay on Resend

MailMoose does not replace everything Resend does. Keep Resend, for now or just for that part of your email, if you need any of these:

- **SMTP.** There is no SMTP relay. An auth service that can only send through SMTP settings cannot send its password resets through MailMoose, unless it can call your own code instead.
- **Attachments** on the email your app sends.
- **Delivery webhooks or delivery status over the API.** Your app is not told when an email is delivered or bounces. Bounces and spam complaints show under **Sent** in the app.
- **An official SDK.** You call the API with `fetch` or any HTTP client.
- **Broadcasts and contact lists.** MailMoose has no newsletter tools.
- **A lot of volume for little money.** The paid plan includes 500 sending recipients a month, then $0.003 each. If sending is most of your bill and you have few domains and inboxes, Resend may well cost less. [Resend pricing, worked out](https://www.mailmoose.dev/blog/resend-pricing) has the numbers.

Password resets sent from your own code work the same as any other email.

## Move your sending

### 1. Set up sending on your domain

Add the domain under **Domains → Add domain** and add the DNS records it shows you: [Connect a domain](https://www.mailmoose.dev/docs/domains/connect-a-domain.md). New domains send through Postmark.

MailMoose's records sit alongside Resend's, so nothing breaks while you test. Follow whether the checklist says add, edit or replace.

Sending does not depend on your MX record. Leave it alone unless you also want this domain's incoming mail in MailMoose.

### 2. Create the address you send from

On Resend, any address on a verified domain can send. On MailMoose, `from` has to be an address that exists, so [create it first](https://www.mailmoose.dev/docs/addresses/create-an-email-address.md).

### 3. Create a send-only key

Open **Settings → API keys** and create a key with only the **Send mail** permission: [API keys](https://www.mailmoose.dev/docs/integrations/api-keys.md). A coding agent connected over MCP can [prepare one for you](https://www.mailmoose.dev/docs/integrations/connect-mcp.md#connect-another-project). Keep it on your server:

```dotenv
MAILMOOSE_API_URL=https://www.mailmoose.dev/api/v1
MAILMOOSE_API_KEY=your_new_project_key
```

### 4. Change the send call

Before, with Resend's SDK:

```ts
await resend.emails.send({
  from: "Acme <hello@acme.com>",
  to: user.email,
  subject: "Reset your password",
  html: `<a href="${resetUrl}">Choose a new password</a>`,
});
```

After, with MailMoose:

```ts
const response = await fetch(`${process.env.MAILMOOSE_API_URL}/messages`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.MAILMOOSE_API_KEY}`,
  },
  body: JSON.stringify({
    from: "Acme <hello@acme.com>",
    to: user.email,
    subject: "Reset your password",
    html: `<a href="${resetUrl}">Choose a new password</a>`,
    campaign: "password_reset",
    // The address comes from your own user records, so it is safe to confirm.
    confirmNewRecipient: true,
  }),
});
if (!response.ok) throw new Error(`MailMoose request failed (${response.status})`);
```

### 5. Test, then switch off Resend

Send a real email to an address you control, and check that it arrives and shows under **Sent**. Then remove `RESEND_API_KEY`. Remove Resend's DNS records once nothing else sends through it.

## Field by field

Send to `POST https://www.mailmoose.dev/api/v1/messages` instead of `POST https://api.resend.com/emails`.

| Resend | MailMoose | Notes |
| --- | --- | --- |
| `from` | `from` | `hello@acme.com` or `Acme <hello@acme.com>`. The address must exist in MailMoose. |
| `to`, `cc`, `bcc` | `to`, `cc`, `bcc` | One address or a list. |
| `subject` | `subject` | |
| `html` | `html` | Can be sent alone. MailMoose builds the plain-text version from it and keeps link addresses. |
| `text` | `text` or `body` | |
| `reply_to` | `reply_to` or `replyTo` | One address or a list. |
| `tags` | `campaign` | One label per email, such as `password_reset`. `tags` is ignored. |
| Response `id` | Response `messageId`, `threadId` | |

Errors have their own shape: see [errors](https://www.mailmoose.dev/docs/integrations/using-the-api.md#errors).

### Not available

These are refused with a `400`, and nothing is sent, rather than being dropped from your email:

- `attachments`
- `headers`
- `scheduled_at`
- `template`, `topic_id` and `react`. If you use React Email, render it to HTML and pass `html`.
- The `Idempotency-Key` header

## What behaves differently

**The first email to each address needs confirming.** A send to an address you have never corresponded with returns `409` unless it includes `confirmNewRecipient: true`. Set it when the address comes from your own verified users, never from an incoming email.

**Retries are not safe.** With no idempotency key, retrying a send that timed out can deliver it twice. Check **Sent** first.

**Sending stops at your limit.** The paid plan includes 500 sending recipients per billing month, then prepaid credit at $0.003 each. There are no automatic charges: with no credit left the API returns `402`. Your account can also reach at most 100 recipients per rolling hour, and each key defaults to 100 messages per rolling 24 hours, which you can change when you create it. If your app needs more, [contact us](https://www.mailmoose.dev/contact). See [sending limits](https://www.mailmoose.dev/docs/reference/sending-limits.md).

**A `201` means the sending provider accepted the email,** not that it reached the inbox.

**Sent keeps a copy.** Every email is stored under **Sent** in your account, including any links in it, such as a password reset link. Make those links expire.
