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

@mailmodo/sdk

v2.0.3

Published

Fire lifecycle events from your app to trigger automated emails via Mailmodo

Readme

@mailmodo/sdk

npm version npm downloads bundle size license

A lightweight, zero-dependency Node.js SDK for firing lifecycle events and updating contact properties on Mailmodo. Use it to trigger automated transactional emails from anywhere in your application — auth flows, webhooks, background jobs, or serverless functions.


Table of Contents


Installation

# npm
npm install @mailmodo/sdk

# yarn
yarn add @mailmodo/sdk

# pnpm
pnpm add @mailmodo/sdk

Requirements: Node.js >=18.0.0


Quick Start

Set your API key as an environment variable and call track — that's it.

export MAILMODO_API_KEY=mm_live_xxxxxxxxxxxxxxxxxxxx
import { track } from '@mailmodo/sdk';

await track('user.signup', {
  email: '[email protected]',
  plan: 'pro',
});

Two API Patterns

The SDK ships two patterns. Use whichever fits your architecture.

Global functions (recommended for most apps)

Reads MAILMODO_API_KEY from the environment automatically and lazily creates a singleton client on the first call.

import { track, identify } from '@mailmodo/sdk';

await track('user.upgraded', { email: '[email protected]', plan: 'enterprise' });
await identify('[email protected]', { company: 'Acme Corp', seats: 50 });

Class-based client (recommended for serverless & multi-tenant)

Instantiate a client explicitly with a config object. Useful when the API key comes from a per-request context (e.g., multi-tenant SaaS, Cloudflare Workers) or when you need different timeout/retry settings per environment.

import { createClient } from '@mailmodo/sdk';

const mailmodo = createClient({
  apiKey: process.env.MAILMODO_API_KEY,
  timeout: 5_000,
  maxRetries: 2,
});

await mailmodo.track('user.signup', { email: '[email protected]' });
await mailmodo.identify('[email protected]', { plan: 'pro' });

Configuration

Both createClient() and new MailmodoClient() accept an optional config object.

| Option | Type | Default | Description | |---|---|---|---| | apiKey | string | process.env.MAILMODO_API_KEY | Your Mailmodo API key. Format: mm_live_xxxxxxxxxxxxxxxxxxxx | | baseUrl | string | https://api.mailmodo.dev | API base URL. Override to http://localhost:3000 for local development. | | timeout | number | 10000 | Request timeout in milliseconds. | | maxRetries | number | 3 | Max retry attempts on retriable errors. Set to 0 to disable retries. |

If apiKey is not provided and MAILMODO_API_KEY is not set in the environment, the constructor throws a MailmodoError immediately.


API Reference

track()

Fires a lifecycle event for a contact. Triggers any automated emails you have configured for that event in Mailmodo.

Global function signature:

function track(event: EventName, properties: TrackProperties): Promise<TrackResponse>

Class method signature:

mailmodoClient.track(event: EventName, properties: TrackProperties): Promise<TrackResponse>

Parameters

| Parameter | Type | Required | Description | |---|---|---|---| | event | EventName | Yes | The lifecycle event name. Autocompletes the 13 known events but accepts any custom string. | | properties | TrackProperties | Yes | An object containing at minimum an email field plus any additional key-value properties to attach to the event. | | properties.email | string | Yes | The contact's email address. Must be a non-empty string. | | properties[key] | string \| number \| boolean \| null | No | Any additional custom properties to send with the event. |

Returns: Promise<TrackResponse>

| Field | Type | Description | |---|---|---| | received | boolean | Whether the event was accepted by the API. | | emailsSent | number (optional) | Number of emails triggered by this event. | | skipped | boolean (optional) | true if the event was received but no email was sent. | | reason | string (optional) | Human-readable explanation when skipped is true. |

Example

const result = await track('user.payment_failed', {
  email: '[email protected]',
  invoiceId: 'inv_abc123',
  amount: 99,
  currency: 'USD',
});

if (result.received) {
  console.log(`Triggered ${result.emailsSent ?? 0} email(s)`);
}

identify()

Updates or creates properties on a contact. Use this to keep your Mailmodo contact records in sync with your application state — plan tier, usage metrics, feature flags, etc.

Global function signature:

function identify(email: string, properties: IdentifyProperties): Promise<IdentifyResponse>

Class method signature:

mailmodoClient.identify(email: string, properties: IdentifyProperties): Promise<IdentifyResponse>

Parameters

| Parameter | Type | Required | Description | |---|---|---|---| | email | string | Yes | The contact's email address. Must be a non-empty string. | | properties | IdentifyProperties | Yes | Key-value pairs of contact properties to set. Values must be string, number, boolean, or null. |

Returns: Promise<IdentifyResponse>

| Field | Type | Description | |---|---|---| | email | string | The contact's email address. | | properties | Record<string, string \| number \| boolean \| null> | The full set of properties stored on the contact after the update. | | status | 'active' \| 'suppressed' | The contact's current suppression status. |

Example

const contact = await identify('[email protected]', {
  plan: 'enterprise',
  seats: 50,
  trialEnded: true,
  accountManager: '[email protected]',
});

console.log(contact.status); // 'active' | 'suppressed'

createClient()

Factory function that returns a new MailmodoClient instance. Prefer this over new MailmodoClient() for cleaner call sites.

function createClient(config?: MailmodoClientConfig): MailmodoClient
import { createClient } from '@mailmodo/sdk';

const mailmodo = createClient({ apiKey: process.env.MAILMODO_API_KEY });

MailmodoClient class

The underlying class behind createClient(). Exposes the same track() and identify() methods.

import { MailmodoClient } from '@mailmodo/sdk';

const mailmodo = new MailmodoClient({
  apiKey: 'mm_live_xxxxxxxxxxxxxxxxxxxx',
  timeout: 8_000,
  maxRetries: 1,
});

Event Names

The SDK ships 13 pre-defined event names with full autocompletion support. You can also pass any arbitrary string for custom events.

| Event Name | Typical Trigger | |---|---| | user.signup | New account created | | user.trial_expiry | Trial period is ending | | user.inactive_14d | No activity in 14 days | | user.upgraded | Upgraded to a paid plan | | user.churned | Subscription cancelled | | user.feature_not_used | Key feature never activated | | user.nps_eligible | User eligible for NPS survey | | user.churn_risk | Identified as churn risk | | user.upsell_eligible | Candidate for plan upgrade | | user.milestone_reached | Hit a usage milestone | | user.password_reset | Password reset requested | | user.payment_failed | Charge failed | | user.renewal_upcoming | Subscription renewing soon |

Custom event names are fully supported:

await track('user.exported_report', { email: '[email protected]' });

Usage Examples

Example 1 — Auth flow (user signup + enrichment)

Fire a user.signup event and immediately enrich the contact with plan and source data.

import { track, identify } from '@mailmodo/sdk';

async function onUserSignup(user: { email: string; plan: string; source: string }) {
  // Fire signup event to trigger the welcome email sequence
  await track('user.signup', {
    email: user.email,
    plan: user.plan,
    signupSource: user.source,
  });

  // Persist properties to the contact record for future segmentation
  await identify(user.email, {
    plan: user.plan,
    signupSource: user.source,
    signedUpAt: new Date().toISOString(),
  });
}

Example 2 — Stripe webhook handler (payment failure)

Trigger a payment-failed email sequence when Stripe fires a invoice.payment_failed event.

import { createClient } from '@mailmodo/sdk';
import type { Request, Response } from 'express';
import Stripe from 'stripe';

const mailmodo = createClient({ apiKey: process.env.MAILMODO_API_KEY });

export async function stripeWebhookHandler(req: Request, res: Response) {
  const event = req.body as Stripe.Event;

  if (event.type === 'invoice.payment_failed') {
    const invoice = event.data.object as Stripe.Invoice;
    const email = invoice.customer_email;

    if (email) {
      await mailmodo.track('user.payment_failed', {
        email,
        invoiceId: invoice.id,
        amount: invoice.amount_due / 100,
        currency: invoice.currency.toUpperCase(),
        attemptCount: invoice.attempt_count,
      });
    }
  }

  res.sendStatus(200);
}

Example 3 — Background job (churn risk scoring)

Run a nightly job to identify at-risk users and fire targeted events.

import { createClient, MailmodoAPIError } from '@mailmodo/sdk';

const mailmodo = createClient({
  apiKey: process.env.MAILMODO_API_KEY,
  maxRetries: 1, // Fail fast in batch jobs
});

async function flagChurnRiskUsers(users: Array<{ email: string; healthScore: number }>) {
  for (const user of users) {
    if (user.healthScore < 30) {
      try {
        await mailmodo.track('user.churn_risk', {
          email: user.email,
          healthScore: user.healthScore,
        });

        await mailmodo.identify(user.email, {
          churnRisk: true,
          healthScore: user.healthScore,
          flaggedAt: new Date().toISOString(),
        });
      } catch (err) {
        if (err instanceof MailmodoAPIError) {
          console.warn(`Skipped ${user.email}: API error ${err.statusCode}`);
        } else {
          throw err; // Re-throw unexpected errors
        }
      }
    }
  }
}

TypeScript Support

The package ships with full TypeScript types — no @types/* package needed. Both ESM (.mjs) and CJS (.cjs) builds include .d.ts declaration files.

Key exported types

import type {
  EventName,          // KnownEventName | (string & {}) — open union with autocomplete
  KnownEventName,     // The 13 pre-defined event name literals
  TrackProperties,    // { email: string; [key: string]: PropertyValue }
  IdentifyProperties, // { [key: string]: PropertyValue }
  TrackResponse,      // { received: boolean; emailsSent?: number; ... }
  IdentifyResponse,   // { email: string; properties: {...}; status: 'active' | 'suppressed' }
  MailmodoClientConfig,
  PropertyValue,      // string | number | boolean | null
} from '@mailmodo/sdk';

How EventName autocompletion works

EventName is defined as KnownEventName | (string & {}). This is a standard TypeScript pattern that preserves literal autocomplete on an open union — your editor will suggest the 13 known event names, but any string is still accepted without a type error.

// ✅ Autocompleted
await track('user.signup', { email: '[email protected]' });

// ✅ Custom events also accepted
await track('user.custom_action', { email: '[email protected]' });

PropertyValue constraint

All custom properties passed to track() and identify() must be string | number | boolean | null. Objects, arrays, and undefined are not accepted, ensuring your payloads remain serialisable and type-safe.

await identify('[email protected]', {
  plan: 'pro',       // ✅ string
  seats: 10,         // ✅ number
  verified: true,    // ✅ boolean
  deletedAt: null,   // ✅ null (clear a property)
  tags: ['a', 'b'],  // ❌ Type error — arrays not allowed
});

Error Handling

The SDK throws structured, typed errors. All error classes extend MailmodoError, so you can catch broadly or narrowly.

Error classes

| Class | Extends | Extra fields | When thrown | |---|---|---|---| | MailmodoError | Error | — | Base class; also thrown for invalid input (e.g., missing email) | | MailmodoAPIError | MailmodoError | statusCode: number | API returned a non-retriable HTTP error (4xx, or 5xx after all retries exhausted) | | MailmodoNetworkError | MailmodoError | cause?: unknown | Network failure or request timeout |

Recommended error handling pattern

import {
  track,
  MailmodoError,
  MailmodoAPIError,
  MailmodoNetworkError,
} from '@mailmodo/sdk';

try {
  await track('user.signup', { email: '[email protected]' });
} catch (err) {
  if (err instanceof MailmodoAPIError) {
    // The API rejected the request (e.g., 400 Bad Request, 401 Unauthorized)
    console.error(`API error ${err.statusCode}: ${err.message}`);

    if (err.statusCode === 401) {
      // Handle invalid API key
    }
  } else if (err instanceof MailmodoNetworkError) {
    // Network is down, request timed out, or all retries exhausted
    console.error('Network error:', err.message, err.cause);
  } else if (err instanceof MailmodoError) {
    // Invalid input — e.g., missing or empty email
    console.error('Invalid call:', err.message);
  } else {
    throw err; // Re-throw anything unexpected
  }
}

instanceof across module systems

The SDK uses Object.setPrototypeOf(this, new.target.prototype) in all error constructors, which ensures instanceof works correctly even when the SDK is loaded as CJS in a mixed ESM/CJS bundle.


Retry & Timeout Behaviour

The SDK automatically retries requests on transient failures. No configuration is required.

| Setting | Default | Config key | |---|---|---| | Timeout per attempt | 10,000 ms | timeout | | Max retry attempts | 3 | maxRetries | | Backoff base | 500 ms | — | | Backoff cap | 30,000 ms | — |

Retriable conditions

Retries are triggered for:

  • HTTP status codes: 408, 429, 500, 502, 503, 504
  • Network errors (DNS failure, connection refused, etc.)
  • Request timeouts (the AbortController fires after timeout ms)

Non-retriable conditions (thrown immediately, no retry):

  • 4xx status codes other than 408 and 429
  • Invalid input (missing email, missing API key)

Backoff strategy

Each retry waits using exponential backoff with jitter:

delay = min(500ms × 2^attempt, 30s) + random jitter up to 500ms

On 429 Too Many Requests, the SDK respects the server's Retry-After header (both the seconds integer and the HTTP-date formats), capped at 30,000 ms.

To disable retries entirely:

const mailmodo = createClient({ apiKey: '...', maxRetries: 0 });

License

MIT © Mailmodo