emailmate
v0.3.0
Published
EmailMate SDK — Resend-compatible email API (emailmate.dev)
Maintainers
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: '[email protected]',
to: '[email protected]',
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 <[email protected]>',
to: '[email protected]',
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 <[email protected]>',
to: '[email protected]',
subject: 'Welcome',
react: <WelcomeEmail name="John" />
});With attachments
await em.emails.send({
from: '[email protected]',
to: '[email protected]',
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: '[email protected]',
to: '[email protected]',
subject: 'Welcome',
html: '<p>Welcome!</p>',
tags: [
{ name: 'category', value: 'onboarding' },
{ name: 'app', value: 'acme' }
]
});Schedule an email
await em.emails.send({
from: '[email protected]',
to: '[email protected]',
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');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: '[email protected]',
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');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 |
Convex Integration
For apps using Convex as their backend:
import { EmailMate } from 'emailmate';
import { internalAction } from './_generated/server';
import { v } from 'convex/values';
const em = new EmailMate(process.env.EMAILMATE_API_KEY!);
export const sendWelcome = internalAction({
args: { email: v.string(), name: v.string() },
handler: async (_ctx, { email, name }) => {
await em.emails.send({
from: 'Acme <[email protected]>',
to: email,
subject: `Welcome, ${name}!`,
html: `<h1>Welcome!</h1><p>Hey ${name}, your account is ready.</p>`
});
},
});Important: In Convex, emails must be sent from internalAction (not mutations). Mutations schedule actions:
// In a mutation
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
