JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 201
  • Score
    100M100P100Q94392F
  • License MIT

EmailMate SDK — Resend-compatible email API (emailmate.dev) + brand-aware react-email templates.

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 emailmate

Quick 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 securely

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

// 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';

License

MIT