npm.io
2.0.2 • Published 6h ago

@mailmodo/sdk

Licence
MIT
Version
2.0.2
Deps
0
Size
63 kB
Vulns
0
Weekly
327

@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: 'alice@example.com',
  plan: 'pro',
});

Two API Patterns

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

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: 'alice@example.com', plan: 'enterprise' });
await identify('alice@example.com', { company: 'Acme Corp', seats: 50 });

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: 'alice@example.com' });
await mailmodo.identify('alice@example.com', { 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: 'alice@example.com',
  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('alice@example.com', {
  plan: 'enterprise',
  seats: 50,
  trialEnded: true,
  accountManager: 'bob@acme.com',
});

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: 'alice@example.com' });

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: 'alice@example.com' });

// ✅ Custom events also accepted
await track('user.custom_action', { email: 'alice@example.com' });
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('alice@example.com', {
  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
import {
  track,
  MailmodoError,
  MailmodoAPIError,
  MailmodoNetworkError,
} from '@mailmodo/sdk';

try {
  await track('user.signup', { email: 'alice@example.com' });
} 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

Keywords