MailcrateDocs

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.

emails/password-reset.tsx
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:

Terminal
npm install -g @mailcrate/cli
mailcrate login --api-key <api_key>

Push one file or a whole folder:

Terminal
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:

Terminal
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:

Terminal
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:

PropValue
Every key of dataThe value you sent, so "data": {"resetUrl": "…"} arrives as props.resetUrl.
contact{ id, email, externalIdentifier, status, properties } for the recipient.
unsubscribeUrlThe recipient's unsubscribe link, or an empty string on transactional message sends. Use it in marketing templates.
sender_addressYour 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:

.github/workflows/emails.yml
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.

On this page