npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

emailmate

v0.3.0

Published

EmailMate SDK — Resend-compatible email API (emailmate.dev)

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: '[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 securely

List 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