Send email from Remix with emailr_
Send transactional email from Remix actions with the emailr_ SDK, keep the client in a .server module and verify delivery webhooks in resource routes.
Remix actions run on the server and receive form submissions directly, which makes them a clean place to send email. This guide keeps the client in a .server module, sends from an action and receives signed webhooks in a resource route.
The same code works in React Router v7 framework mode, which uses the same action and resource route conventions.
Set up emailr_ with Remix
- 01
Create an API key
Create a key under API keys in the dashboard. Keys start with
et_live_and are scoped to one domain workspace. Store it asEMAILR_API_KEYin your secret manager or environment, never in client-side code. - 02
Add and verify your sending domain
In the dashboard, open Domains and add the domain you send from. A subdomain such as
mail.yourcompany.comkeeps transactional reputation separate from your corporate mailbox.Emailr generates the DNS records: a DKIM TXT record on the
emailr._domainkeyselector, an SPF TXT record, and an MX + TXT pair on thesendsubdomain used as the MAIL FROM / Return-Path. A DMARC record is optional but recommended.Emailr's DKIM selector and
sendsubdomain don't collide with other providers' records, so you can add them before cutting over and send from both providers during the migration. A domain can only have one SPF record: if one already exists, merge Emailr'sinclude:into it instead of adding a second record. - 03
Install the SDK
Install the package and set
EMAILR_API_KEYin your environment.Terminalbashnpm install emailr - 04
Create a server-only client
The
.server.tssuffix tells the compiler never to include this module in browser bundles, so the key can't leak.app/lib/emailr.server.tstsimport { Emailr } from 'emailr' export const emailr = new Emailr({ apiKey: process.env.EMAILR_API_KEY! }) - 05
Send from an action
Read the submitted form, send, and return data to the page. Errors thrown by the SDK are
EmailrErrorinstances with astatusCode, so you can show a friendly message for validation failures.app/routes/contact.tsxtsximport type { ActionFunctionArgs } from '@remix-run/node' import { json } from '@remix-run/node' import { emailr } from '~/lib/emailr.server' export async function action({ request }: ActionFunctionArgs) { const form = await request.formData() await emailr.emails.send({ from: 'Website <[email protected]>', to: '[email protected]', reply_to_email: String(form.get('email')), subject: 'New contact form message', text: String(form.get('message')), }) return json({ sent: true }) } - 06
Receive webhooks in a resource route
A route module with only an
actionexport is a resource route. Readrequest.text()before parsing so the signature check sees the raw body.app/routes/webhooks.emailr.tstsimport type { ActionFunctionArgs } from '@remix-run/node' import { createHmac, timingSafeEqual } from 'node:crypto' export async function action({ request }: ActionFunctionArgs) { const body = await request.text() const signature = request.headers.get('x-emailr-signature') ?? '' const expected = createHmac('sha256', process.env.EMAILR_WEBHOOK_SECRET!) .update(body) .digest('hex') if (signature.length !== expected.length || !timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) { return new Response('Invalid signature', { status: 401 }) } const { event, data } = JSON.parse(body) return new Response('ok') }
Tips
- On Cloudflare-hosted Remix apps, read the key from the
context.cloudflare.envbinding instead ofprocess.env, and verify webhooks with Web Crypto. - For password resets and magic links, send from the action that creates the token so a failed send surfaces immediately.
Frequently asked questions
Where should the emailr_ client live in a Remix app?
In a .server.ts module. Remix excludes .server modules from browser bundles, so your API key never reaches the client.
Does this work with React Router v7?
Yes. Actions and resource routes work the same way in React Router v7 framework mode; only the import paths for types change.
How do I handle bounces in Remix?
Point a transactional webhook at a resource route, verify the X-Emailr-Signature header against the raw body, and handle email.bounced events.
Can I send HTML built with React?
Yes. Render your email component to an HTML string and pass it as the html field.
Keep going
Documentation
Related guides
- Send email from Next.jsSend transactional email from Next.js App Router route handlers and server actions with the emailr_ SDK, and verify delivery webhooks safely.
- Send email from NuxtSend transactional email from Nuxt server routes with the emailr_ SDK, keep keys in runtimeConfig and verify delivery webhooks with raw bodies.
- Send email from Cloudflare WorkersSend transactional email from Cloudflare Workers with the fetch-based emailr_ SDK, store keys as Worker secrets and verify webhooks with Web Crypto.
Start sending in minutes
Create a free account, verify your domain and send your first email. No credit card required.