Send emails with Remix
Transactional and marketing email from a Remix action. The key stays on the server, and retries never send twice.
✓Free to start. Unlimited contacts on every tier.
One request, complete trace
Request
POST /api/v1/emails
Authorization: Bearer aem_…
Idempotency-Key: welcome-8f21
{ "to": "[email protected]", "template": "welcome" }
202 Accepted
{ "id": "msg_7ca31", "status": "queued" }
[ 01 / 05 ]Quickstart
How do I send email from Remix?
Create a key, keep it on the server, and call one endpoint from a Remix action.
Get an API key
Open Settings → API & MCP and create a key with the Send + manage permission (the default). It starts with aem_ and is shown once.
Store it as a server-side environment variable
Set AEM_KEY=aem_… in your server environment. The .server.ts suffix keeps this file out of the browser build.
app/email.server.tsexport const EMAIL_URL = "https://sendandretain.com/api/v1/emails"; export const authHeaders = (idempotencyKey: string) => ({ Authorization: `Bearer ${process.env.AEM_KEY}`, "Content-Type": "application/json", "Idempotency-Key": idempotencyKey, });Send the email
The action posts to the email API. A repeated form submission with the same Idempotency-Key won't send a second email.
app/routes/welcome.tsximport type { ActionFunctionArgs } from "@remix-run/node"; import { EMAIL_URL, authHeaders } from "~/email.server"; export async function action({ request }: ActionFunctionArgs) { const form = await request.formData(); const res = await fetch(EMAIL_URL, { method: "POST", headers: authHeaders(`welcome-${form.get("userId")}`), body: JSON.stringify({ to: form.get("email"), template: "welcome" }) }); return Response.json(await res.json(), { status: res.status }); }Check the response
A 202 means queued, not delivered. Keep the id, then follow delivery in the message log or through a signed webhook.
202 Accepted{ "id": "msg_7ca31", "status": "queued" }
[ 02 / 05 ]Where it goes
Where should email code live in your Remix app?
Actions and resource routes
Use an action for form submissions. Use a resource route for other services and for webhooks.
Events after the action
Send the event after the action saves the change, not because a component rendered.
A resource route for webhooks
Read await request.text() so the signature check sees the original body.
[ 03 / 05 ]Marketing email
Can I send marketing emails from Remix too?
Yes. Send an event from the same action when a user signs up, starts a trial, or goes quiet, and an automation sends the right email. Automations start paused, and every email goes to one person: no newsletter blasts.
- Transactional and marketing email share one contact record and one unsubscribe list.
- Unsubscribes, bounces, and complaints are suppressed for you before every send.
- Templates are react.email components; published versions never change under you.
[ 04 / 05 ]Example prompts
What can you ask your coding agent to do in Remix?
Claude Code, Cursor, and Codex can do these over the Send & Retain MCP server, with your repo open. Automations they build stay paused until you turn them on.
Welcome email
“Add a welcome email to our Remix signup action using Send & Retain, with an idempotency key.”
Sign-in link
“Add a sign-in link email to our Remix login action, render it in our brand, and send me a test.”
Webhook
“Add a Remix resource route that verifies Send & Retain delivery webhooks and flags bounced users.”
Trial automation
“Send a trial_started event from our Remix app and build a paused trial-conversion automation for it.”
[ 05 / 05 ]Related
Keep building with Remix
Node.js
Send emails from any Node.js or TypeScript server with fetch.
Email API
One REST API for every send, with scoped keys and signed webhooks.
Transactional email
Receipts, resets, and alerts on the same contact record as your automations.
Next.js
Send emails from Next.js Route Handlers and Server Actions.
Questions people ask us
Send your first email from Remix today.
Create a key, paste the code into a Remix action, and watch the email land in the message log.