Templates as Code
Write email templates as React Email components in your own repository, push them with the CLI, and send them by slug.
If your team would rather keep email templates next to the code that sends them, write them as React Email components. You review them in pull requests, push them with @mailcrate/cli, and send them with the same API call as any other template.
Write the template
A template file exports a templateSlug, which names it in Mailcrate, and the email component, as Template or as the default export.
import { Body, Button, Container, Head, Html, Preview, Text } from '@react-email/components';
export const templateSlug = 'password-reset';
interface PasswordResetProps {
// From the `data` of the send request.
resetUrl: string;
expiresInMinutes: number;
// Added by Mailcrate on every send.
contact: { email: string; properties: Record<string, unknown> };
}
export const Template = ({
resetUrl = 'https://app.example.com/reset/sample',
expiresInMinutes = 30,
contact = { email: 'ada@example.com', properties: { firstName: 'Ada' } },
}: PasswordResetProps) => (
<Html lang="en">
<Head />
<Preview>Reset your password</Preview>
<Body style={{ fontFamily: 'sans-serif' }}>
<Container>
<Text>Hi {String(contact.properties.firstName ?? 'there')},</Text>
<Text>Someone asked to reset your password.</Text>
<Button href={resetUrl}>Choose a new password</Button>
<Text>The link expires in {expiresInMinutes} minutes.</Text>
</Container>
</Body>
</Html>
);
export default Template;The default values are what the React Email preview server shows while you work on the file (npx react-email dev). Mailcrate passes real values at send time.
Push it
Install the CLI and log in with an API key that has the email-template:write scope:
npm install -g @mailcrate/cli
mailcrate login --api-key <api_key>Push one file or a whole folder:
mailcrate push ./emails/The CLI compiles each file, matches it to a Mailcrate template by its templateSlug, and uploads it. A template that does not exist yet is created after you confirm; add -y to create without asking, which is what you want in CI. A push that changes nothing is reported as unchanged.
Set the subject and sender once
Pushing uploads the email body only. The subject, sender and other headers live on the template in Mailcrate, so set them once after the first push, either in the app (Emails, open the template, Settings) or with the CLI:
mailcrate update-email-template password-reset \
--subject "Reset your password" \
--sender-identity-id <sender_identity_id>Later pushes leave these settings alone. The subject can use Liquid variables, such as Your order {{ orderNumber }}.
Send it
Send by slug, exactly like any other template:
curl -X POST https://api.mailcrate.io/public/sending/mail \
-H "Authorization: Api-Key $MAILCRATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contactEmail": "ada@example.com",
"templateId": "password-reset",
"lane": "transactional",
"data": { "resetUrl": "https://app.example.com/reset/8f2c", "expiresInMinutes": 30 }
}'The props your template gets
At send time the component is rendered once with these props:
| Prop | Value |
|---|---|
Every key of data | The value you sent, so "data": {"resetUrl": "…"} arrives as props.resetUrl. |
contact | { id, email, externalIdentifier, status, properties } for the recipient. |
unsubscribeUrl | The recipient's unsubscribe link, or an empty string on transactional message sends. Use it in marketing templates. |
sender_address | Your workspace's postal address, for the footer. |
Props are serialised JSON, so they are strings, numbers, booleans, objects and arrays. A missing data key arrives as undefined, and your component's default value applies.
Pushing from CI
Keep your templates in sync with the branch that ships them by pushing on every merge:
name: Push email templates
on:
push:
branches: [main]
paths: ['emails/**']
jobs:
push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx @mailcrate/cli push ./emails/ -y
env:
MAILCRATE_API_KEY: ${{ secrets.MAILCRATE_API_KEY }}Every push is saved as a new version in the template's history, and an email that is already queued keeps the version it was sent with.
What a template can use
Templates are rendered in a sandbox, so a few things are not available: react-dom, Node built-ins such as fs, eval, and functions passed as props. Components from @react-email/* and jsx-email, hooks, and context all work. A template is limited to 10,000 nodes and 100 levels of nesting. The CLI page has the full command reference.