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 pushMailmanny 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:
- compiles it with your project's own esbuild (or tsx, if that's what you have), using your tsconfig paths;
- renders the default export with your
@react-email/render, passing the component'sPreviewProps; - 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#
emails/├── welcome.tsx → welcome├── receipt.tsx → receipt├── auth/│ └── reset.tsx → auth.reset├── _components/ (skipped: starts with _)│ └── footer.tsx└── static/ (skipped) └── logo.pngEvery .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.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#
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 workspacePush from CI#
Create a key with only the Push templates permission and store it as a CI secret:
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#
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#
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}}" />,});curl -X PUT https://www.mailmanny.com/api/v1/templates/welcome \ -H "Authorization: Bearer $MAILMANNY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Welcome email", "subject": "Welcome to Manny'"'"'s Bakery", "html": "<html>…</html>", "text": "Welcome! …" }'