Skip to content
Guides

Templates & the CLI

Keep your emails in code, as React Email components in your repo. One command renders the whole folder and uploads it to Mailmanny, where you can send it by name and preview it in the app.

Push your React Email folder#

Run this in your project (where react and @react-email/render are installed). The key needs the Push templates permission, and pushing templates is on paid plans (Essentials and up, from $9 a month). Sending React Email with emails.send works on every plan.

MAILMANNY_API_KEY=mm_live_… npx mailmanny templates push
Output
Mailmanny pushing 3 templates from emails → https://www.mailmanny.com/api/v1  ✓ welcome         created  4.1 KB  “Welcome to Manny's Bakery”  ✓ receipt         updated  6.8 KB  “Your receipt”  ✓ auth.reset      created  1.2 KB  “Reset your password”3 pushed. Use them with template_id: "<id>" when sending.

For every email file the CLI:

  1. compiles it with your project's own esbuild (or tsx, if that's what you have), using your tsconfig paths;
  2. renders the default export with your @react-email/render, passing the component's PreviewProps;
  3. uploads the HTML and a plain-text version as a template named after the file.

Pushing again updates the template in place, so it's safe to run on every deploy. The CLI reads MAILMANNY_API_KEY (and MAILMANNY_BASE_URL) from your shell, .env.local or .env.

Which files become templates#

Files
emails/├── welcome.tsx          → welcome├── receipt.tsx          → receipt├── auth/│   └── reset.tsx        → auth.reset├── _components/         (skipped: starts with _)│   └── footer.tsx└── static/              (skipped)    └── logo.png

Every .tsx, .jsx, .ts or .js file with a default-exported component is a template. Its id is its path without the extension, with folders joined by dots. Folders starting with _ or ., plus static and components, are skipped, like React Email's preview does.

What a template file can export#

emails/welcome.tsxTSX
// emails/welcome.tsximport { Body, Button, Container, Heading, Html, Preview, Text } from "@react-email/components";
// Optional: the default subject (a string, or a function of the props)export const subject = "Welcome to Manny's Bakery";
// Optional: the template's display name and preview text in Mailmannyexport const meta = { name: "Welcome email", previewText: "Your first croissant is on us" };
export default function WelcomeEmail({ firstName }: { firstName: string }) {  return (    <Html>      <Preview>Your first croissant is on us</Preview>      <Body>        <Container>          <Heading>Welcome, {firstName}!</Heading>          <Text>Show this email at the counter for a free croissant.</Text>          <Button href="https://mannysbakery.com/menu">See the menu</Button>        </Container>      </Body>    </Html>  );}
// The CLI renders with these props. Merge tags stay in the HTML and are filled in per email at send time.WelcomeEmail.PreviewProps = { firstName: "{{first_name|friend}}" };
defaultcomponentRequired
The email. Rendered with PreviewProps.
subjectstring | (props) => string
The template's default subject.
meta.namestring
Display name in Mailmanny. Default: the file name, humanized.
meta.previewTextstring
Preview text shown in the template list.
meta.propsobject
Props to render with instead of PreviewProps.

Personalize with merge tags#

Templates render once, so per-person values are merge tags: {{first_name}}, {{email}}, {{fields.COMPANY}}, with an optional fallback after a pipe: {{first_name|friend}}. Put them in PreviewProps as shown above. When you send through the API, variables fill them in; in campaigns and automations, each contact's own data does.

Options#

Terminal
npx mailmanny templates push --dir src/emails     # a different foldernpx mailmanny templates push --only welcome,receipt   # just thesenpx mailmanny templates push --dry-run              # render and check, upload nothingnpx mailmanny templates list                        # what's in your workspace

Push from CI#

Create a key with only the Push templates permission and store it as a CI secret:

.github/workflows/email-templates.ymlYAML
name: Push email templateson:  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, cache: npm }      - run: npm ci      - run: npx mailmanny templates push        env:          MAILMANNY_API_KEY: ${{ secrets.MAILMANNY_TEMPLATES_KEY }}

Send a template#

TypeScript
await mm.emails.send({  from: "Manny's Bakery <hello@mannysbakery.com>",  to: "sam@example.com",  templateId: "welcome",  variables: { first_name: "Sam" },});

See Sending email for details. Pushed templates also show up in Templates in the app, ready to use in campaigns.

Templates API#

GET/api/v1/templatesPermission: templates:read
GET/api/v1/templates/{id_or_external_id}Permission: templates:read
PUT/api/v1/templates/{external_id}Permission: templates:write
DELETE/api/v1/templates/{id_or_external_id}Permission: templates:write

PUT creates or replaces an HTML template (201 when new, 200 when updated). It takes html (required), text (generated from the HTML if left out), name, subject and preview_text. Templates built in the drag-and-drop editor can be read and sent, but not overwritten: a PUT to one returns 409 conflict.

await mm.templates.push({  externalId: "welcome",  subject: "Welcome to Manny's Bakery",  react: <WelcomeEmail firstName="{{first_name|friend}}" />,});