Package Exports
- emailmate
- emailmate/convex
- emailmate/package.json
- emailmate/react
- emailmate/templates
- emailmate/templates/blocks
- emailmate/templates/components
- emailmate/templates/tailwind-preset
- emailmate/templates/tokens
Readme
emailmate
Email API that doesn't eat your margins.
New here? Simple SDK. React Email baked in.
Coming from Resend? One line change. No refactor.
Install
npm install emailmateQuick Start
import { EmailMate } from 'emailmate';
const emailmate = new EmailMate('em_xxxxxxxxxxxx');
await emailmate.emails.send({
from: 'you@yourapp.com',
to: 'user@example.com',
subject: 'Welcome',
html: '<h1>Welcome aboard!</h1>'
});Coming from Resend?
One line. That's it.
// Before
import { Resend } from 'resend';
// After
import { Resend } from 'emailmate';
// Your code? Untouched. Same API. Same types. Same everything.
const resend = new Resend('em_xxxxxxxxxxxx');
await resend.emails.send({ ... });Configuration
import { EmailMate } from 'emailmate';
// Default (emailmate.dev cloud)
const em = new EmailMate('em_xxxxxxxxxxxx');
// Self-hosted / BYOS (Bring Your Own SES)
const em = new EmailMate('em_xxxxxxxxxxxx', {
baseUrl: 'https://your-instance.com'
});| Parameter | Type | Default | Description |
|---|---|---|---|
apiKey |
string |
required | Your API key (em_...) |
config.baseUrl |
string |
https://www.emailmate.dev |
API base URL |
Emails
Send an email
const { id } = await em.emails.send({
from: 'Acme <hello@acme.com>',
to: 'user@example.com',
subject: 'Welcome to Acme',
html: '<h1>Welcome!</h1><p>Thanks for signing up.</p>'
});Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
from |
string |
Yes | Sender (Name <email> or email) |
to |
string | string[] |
Yes | Recipient(s) |
subject |
string |
Yes | Email subject |
html |
string |
No* | HTML body |
text |
string |
No | Plain text body (auto-generated if omitted) |
react |
ReactElement |
No* | React Email component |
reply_to |
string | string[] |
No | Reply-to address(es) |
cc |
string | string[] |
No | CC recipients |
bcc |
string | string[] |
No | BCC recipients |
headers |
Record<string, string> |
No | Custom headers |
attachments |
Attachment[] |
No | File attachments |
tags |
Tag[] |
No | Email tags for tracking |
scheduled_at |
string |
No | ISO 8601 datetime for scheduled send |
*Provide either html or react, not both.
Response: { id: string, object: "email" }
With React Email
import { WelcomeEmail } from './emails/welcome';
await em.emails.send({
from: 'Acme <hello@acme.com>',
to: 'user@example.com',
subject: 'Welcome',
react: <WelcomeEmail name="John" />
});With attachments
await em.emails.send({
from: 'billing@acme.com',
to: 'user@example.com',
subject: 'Your invoice',
html: '<p>Invoice attached.</p>',
attachments: [{
filename: 'invoice.pdf',
content: Buffer.from(pdfBytes),
content_type: 'application/pdf'
}]
});With tags
await em.emails.send({
from: 'hello@acme.com',
to: 'user@example.com',
subject: 'Welcome',
html: '<p>Welcome!</p>',
tags: [
{ name: 'category', value: 'onboarding' },
{ name: 'app', value: 'acme' }
]
});Schedule an email
await em.emails.send({
from: 'hello@acme.com',
to: 'user@example.com',
subject: 'Reminder',
html: '<p>Don\'t forget!</p>',
scheduled_at: '2025-04-01T10:00:00Z'
});Get an email
const email = await em.emails.get('em_xxxx');
// { id, object, to, from, subject, html, text, created_at, last_event }Response:
| Field | Type | Description |
|---|---|---|
id |
string |
Email ID |
to |
string[] |
Recipients |
from |
string |
Sender |
subject |
string |
Subject |
html |
string |
HTML body |
text |
string |
Plain text body |
created_at |
string |
ISO 8601 timestamp |
last_event |
string |
Latest event (sent, delivered, opened, bounced, etc.) |
Cancel a scheduled email
const { id, canceled } = await em.emails.cancel('em_xxxx');Batch send
Send up to 100 emails in a single HTTP call.
const { data } = await em.emails.batch([
{ from, to: 'a@x.com', subject: 'Hi', html: '<p>Hi</p>' },
{ from, to: 'b@x.com', subject: 'Hello', html: '<p>Hello</p>' },
]);
// data: [{ id: 'em_...' }, { id: 'em_...' }]List emails (paginated)
const page = await em.emails.list({ limit: 50, after: 'em_cursor' });
// { object: 'list', data: Email[], has_more: boolean, next_cursor?: string }Domains
Add a domain
const domain = await em.domains.create({ name: 'acme.com' });
// { id, name, status: "pending", records: [...] }List domains
const { data } = await em.domains.list();
// data: Domain[]Get domain details
const domain = await em.domains.get('dom_xxxx');Domain object:
| Field | Type | Description |
|---|---|---|
id |
string |
Domain ID |
name |
string |
Domain name |
status |
"pending" | "verified" | "failed" |
Verification status |
created_at |
string |
ISO 8601 timestamp |
records |
DnsRecord[] |
DNS records to configure |
DnsRecord:
| Field | Type | Description |
|---|---|---|
type |
string |
TXT, CNAME, MX |
name |
string |
Record name |
value |
string |
Record value |
status |
"pending" | "verified" |
Record status |
Verify a domain
const domain = await em.domains.verify('dom_xxxx');Delete a domain
await em.domains.delete('dom_xxxx');Audiences
Create an audience
const audience = await em.audiences.create({ name: 'Newsletter' });
// { id, name, created_at }List audiences
const { data } = await em.audiences.list();Get an audience
const audience = await em.audiences.get('aud_xxxx');Delete an audience
await em.audiences.delete('aud_xxxx');Contacts
Add a contact
const contact = await em.contacts.create('aud_xxxx', {
email: 'user@example.com',
first_name: 'Jane',
last_name: 'Doe',
unsubscribed: false
});| Field | Type | Required | Description |
|---|---|---|---|
email |
string |
Yes | Contact email |
first_name |
string |
No | First name |
last_name |
string |
No | Last name |
unsubscribed |
boolean |
No | Opt-out status (default: false) |
List contacts
const { data } = await em.contacts.list('aud_xxxx');Get a contact
const contact = await em.contacts.get('aud_xxxx', 'con_xxxx');Update a contact
await em.contacts.update('aud_xxxx', 'con_xxxx', {
first_name: 'Janet',
unsubscribed: true
});Delete a contact
await em.contacts.delete('aud_xxxx', 'con_xxxx');API Keys
Create an API key
const { id, token } = await em.apiKeys.create({
name: 'Production',
permission: 'full_access' // or 'sending_access'
});
// token is only shown once — store it securelyList API keys
const { data } = await em.apiKeys.list();Delete an API key
await em.apiKeys.delete('key_xxxx');Templates
Server-stored, reusable email templates (distinct from the
emailmate/templates react-email components).
const t = await em.templates.create({
name: 'Welcome',
slug: 'welcome-v1',
html: '<h1>Welcome {{ name }}</h1>',
variables: ['name'],
});
await em.templates.list();
await em.templates.get(t.id);
await em.templates.update(t.id, { html: '<h1>Hi {{ name }}</h1>' });
await em.templates.delete(t.id);Webhooks
const w = await em.webhooks.create({
url: 'https://api.acme.com/email-webhook',
events: ['email.delivered', 'email.bounced', 'email.opened'],
});
await em.webhooks.list();
await em.webhooks.test(w.id); // trigger a sample event
await em.webhooks.update(w.id, { status: 'disabled' });
await em.webhooks.delete(w.id);Broadcasts
const b = await em.broadcasts.create({
audience_id: 'aud_xxxx',
from: 'Acme <hello@acme.com>',
subject: 'New release',
html: '<p>It ships Monday.</p>',
});
await em.broadcasts.send(b.id, { scheduled_at: '2026-05-01T14:00:00Z' });
await em.broadcasts.cancel(b.id);Retries & timeouts
The SDK retries 5xx, 429, and network errors automatically.
const em = new EmailMate('em_xxxx', {
maxRetries: 3, // default: 3
retryBackoff: 'exponential', // 'exponential' | 'linear', default: 'exponential'
timeout: 30_000, // per-request ms, default: 30_000
onRetry: (attempt, error) => console.warn('retry', attempt, error),
onError: (error) => console.error('final', error),
onResponse: (res) => console.log(res.status),
});Every method accepts a { signal } for cancellation:
const controller = new AbortController();
const p = em.emails.send(payload, { signal: controller.signal });
controller.abort();EmailMateAPIError now carries status, code, requestId, and attempt.
React hooks (emailmate/react)
import { EmailMateProvider, useSendEmail, useDomains } from 'emailmate/react';
function Root({ children }) {
return (
<EmailMateProvider apiKey={process.env.NEXT_PUBLIC_EMAILMATE_KEY!}>
{children}
</EmailMateProvider>
);
}
function SendForm() {
const { sendEmail, isPending, error, lastResult } = useSendEmail();
return (
<button
disabled={isPending}
onClick={() =>
sendEmail({
from: 'hello@acme.com',
to: 'you@example.com',
subject: 'Hi',
html: '<p>Hi!</p>',
})
}
>
{isPending ? 'Sending…' : 'Send'}
</button>
);
}| Hook | Signature |
|---|---|
useSendEmail() |
{ sendEmail, isPending, error, lastResult, reset } |
useEmail(id, opts?) |
{ data, isLoading, error, refetch } — opts.pollInterval, opts.enabled |
useDomains() |
{ data, isLoading, error, refetch } |
useDomain(id) |
{ data, isLoading, error, refetch } |
useApiKeys() |
{ data, isLoading, error, refetch } |
React ≥ 18 is required (peer dep).
HTML Helpers
Built-in utilities for building email templates without React Email.
import { wrap, btn, p, greeting, center, hint, htmlToText } from 'emailmate';
const html = wrap(
greeting('Jane') +
p('Thanks for signing up. Your account is ready.') +
center(btn('https://acme.com/dashboard', 'Go to Dashboard')) +
hint('If you didn\'t create this account, ignore this email.'),
{ brand: 'Acme', tagline: 'Ship faster', url: 'https://acme.com', domain: 'acme.com' }
);
// Auto-generate plain text from HTML
const text = htmlToText(html);
await em.emails.send({ from, to, subject, html, text });| Function | Description |
|---|---|
wrap(body, opts?) |
Full HTML email wrapper with brand footer |
btn(href, label, color?) |
CTA button (default: dark) |
p(text) |
Styled paragraph |
greeting(name) |
"Hi {name}," paragraph |
center(content) |
Center-aligned wrapper |
hint(text) |
Small gray footnote text |
htmlToText(html) |
Strip HTML → plain text |
React Email Templates (emailmate/templates)
For richer, brand-aware transactional emails, EmailMate ships a set of production-ready react-email templates with 12 brand palettes, zod-validated props, and a Tailwind preset.
import { renderTemplate } from 'emailmate/templates';
import { EmailMate } from 'emailmate';
const em = new EmailMate(process.env.EMAILMATE_API_KEY!);
const { html, text } = await renderTemplate('welcome', {
appKey: 'bookstand',
userName: 'Jimmy',
});
await em.emails.send({
from: 'BookStand <hello@bookstand.app>',
to: 'user@example.com',
subject: 'Welcome to BookStand',
html,
text,
});Or render a component directly for full React-level type inference:
import { createElement } from 'react';
import { Welcome, renderToEmail } from 'emailmate/templates';
const { html, text } = await renderToEmail(
createElement(Welcome, { appKey: 'bookstand', userName: 'Jimmy' }),
);Available templates
| Slug | Component | Category | Purpose |
|---|---|---|---|
welcome |
Welcome |
lifecycle | Sent when a user activates an app |
order-confirmation |
OrderConfirmation |
commerce | Buyer receipt (ebook + physical) |
password-reset |
PasswordReset |
auth | Reset-password link with expiry |
Each template exports a default React component, a <Name>Variables zod
schema, and a <Name>Meta object ({ slug, name, description, category, defaultProps }). A central templates registry is also exposed for
dashboard/preview UIs:
import { templates, getTemplate } from 'emailmate/templates';
for (const [slug, { component, variables, meta }] of Object.entries(templates)) {
/* ... */
}Primitives
import {
Container, BrandHeader, BrandFooter,
Button, Heading, Paragraph, BrandLink, Code, Hr,
OrderItem, LinkPreview, MetadataTable,
} from 'emailmate/templates/components';All primitives are appKey-aware — pass the brand key and colors flow through
automatically.
Brand tokens
import {
tokens, BRAND_TOKENS, APP_KEYS, type AppKey,
} from 'emailmate/templates/tokens';
const t = tokens('bookstand');
// ^ { name, domain, supportEmail, tagline, accent, text, muted, ... }Tailwind preset (optional)
import { Tailwind } from '@react-email/tailwind';
import { tailwindPreset } from 'emailmate/templates/tailwind-preset';
export default function MyEmail() {
return (
<Tailwind config={tailwindPreset}>
{/* className="bg-bookstand-accent text-bookstand-accent-text" works */}
</Tailwind>
);
}Every brand exposes: <brand>-accent, <brand>-accent-text, <brand>-text,
<brand>-muted, <brand>-background, <brand>-surface, <brand>-border.
Peer dependencies
Templates render React elements, so react (>=18) and react-dom (>=18) are
peer dependencies. They are marked optional, so consumers using only
the SDK + HTML helpers are not forced to install them.
Convex Integration (emailmate/convex)
For apps using Convex as their backend. Two helpers are available:
1. Generic factory (recommended)
// convex/email/acme.ts
import { createConvexEmailSender } from 'emailmate/convex';
export const sendAcmeEmail = createConvexEmailSender({
apiKey: process.env.EMAILMATE_API_KEY!,
defaultFrom: 'Acme <hello@acme.com>',
defaultReplyTo: 'support@acme.com',
defaultTags: [{ name: 'app', value: 'acme' }],
});
// convex/auth.ts
import { internalAction } from './_generated/server';
import { v } from 'convex/values';
import { sendAcmeEmail } from './email/acme';
export const sendWelcome = internalAction({
args: { email: v.string(), name: v.string() },
handler: async (_ctx, { email, name }) => {
await sendAcmeEmail({
to: email,
subject: `Welcome, ${name}!`,
html: `<h1>Welcome!</h1><p>Hey ${name}, your account is ready.</p>`,
});
},
});2. Low-level one-shot helper
import { sendViaEmailMate } from 'emailmate/convex';
await sendViaEmailMate({
apiKey: process.env.EMAILMATE_API_KEY!,
from: 'Acme <hello@acme.com>',
to: 'user@example.com',
subject: 'Hi',
html: '<p>Hi</p>',
});Deprecated (removed in v0.6.0)
The per-app senders letterbox, versionpill, snowlabs, partners,
bookshelves, authorkit, emailmate, and sendViaPlatform are deprecated
as of v0.5.0. They still work but emit @deprecated JSDoc warnings. Migrate
to createConvexEmailSender:
// Before
import { bookshelves } from 'emailmate/convex';
await bookshelves.send(ctx, internal, { to, subject, html });
// After
const send = createConvexEmailSender({
apiKey: process.env.EMAILMATE_API_KEY!,
defaultFrom: 'Bookshelves <hello@bookshelves.me>',
});
await send({ to, subject, html });Important: In Convex, emails must be sent from internalAction (not
mutations). Mutations schedule actions:
ctx.scheduler.runAfter(0, internal.emails.sendWelcome, { email, name });Error Handling
try {
await em.emails.send({ ... });
} catch (error) {
// { code: 'validation_error', message: 'Missing required field: to' }
console.error(error.code, error.message);
}| Error Code | Description |
|---|---|
validation_error |
Missing or invalid fields |
unauthorized |
Invalid API key |
forbidden |
Domain not verified |
not_found |
Resource doesn't exist |
rate_limit_exceeded |
Too many requests |
internal_error |
Server error |
Types
All types are exported:
import type {
SendEmailOptions,
SendEmailResponse,
Email,
Domain,
DnsRecord,
Audience,
Contact,
ApiKey,
Attachment,
Tag,
EmailMateConfig,
EmailMateError,
} from 'emailmate';Links
License
MIT