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

nodemailer-sendpulse-transport

v0.1.0

Published

A Nodemailer transport for sending transactional emails through the SendPulse SMTP API.

Downloads

174

Readme

nodemailer-sendpulse-transport

NPM Downloads npm version License codecov Test Publish to NPM

A Nodemailer transport for sending transactional emails through the SendPulse SMTP API.

It integrates directly with Nodemailer's transport system while using SendPulse's HTTP API under the hood.

Supports:

  • Static SendPulse API keys
  • OAuth 2.0 client credentials
  • Automatic OAuth token caching
  • Automatic OAuth token refresh on 401
  • HTML and plain-text emails
  • to, cc, bcc, and replyTo
  • Attachments
  • SendPulse templates
  • Nodemailer-compatible SentMessageInfo

Framework Support

While nodemailer-sendpulse-transport is completely framework agnostic, it is fully supported and can be used in:

  • Arcstack
  • H3ravel

Installation

npm install nodemailer-sendpulse-transport nodemailer

Using pnpm:

pnpm add nodemailer-sendpulse-transport nodemailer

Using Yarn:

yarn add nodemailer-sendpulse-transport nodemailer

Usage

Create a SendPulseTransport and pass it to Nodemailer:

import nodemailer from 'nodemailer';
import { SendPulseTransport } from 'nodemailer-sendpulse-transport';

const transport = new SendPulseTransport({
  apiKey: process.env.SENDPULSE_API_KEY!,
});

const mailer = nodemailer.createTransport(transport);

await mailer.sendMail({
  from: {
    name: 'Acme',
    address: '[email protected]',
  },

  to: '[email protected]',

  subject: 'Hello from SendPulse',

  html: `
        <h1>Hello</h1>
        <p>This email was sent through SendPulse.</p>
    `,

  text: 'Hello. This email was sent through SendPulse.',
});

Authentication

SendPulse supports both static API keys and OAuth client credentials.

API Key

For applications using a SendPulse API key:

const transport = new SendPulseTransport({
  apiKey: process.env.SENDPULSE_API_KEY!,
});

The API key is sent directly as a Bearer token:

Authorization: Bearer <api-key>

No additional authentication requests are made.

OAuth

You can alternatively authenticate using a SendPulse client ID and client secret:

const transport = new SendPulseTransport({
  clientId: process.env.SENDPULSE_CLIENT_ID!,
  clientSecret: process.env.SENDPULSE_CLIENT_SECRET!,
});

The transport automatically:

  1. Requests an access token from SendPulse.
  2. Caches the token in memory.
  3. Reuses the token for subsequent messages.
  4. Refreshes the token before its expiration.
  5. Requests a fresh token and retries once when SendPulse unexpectedly responds with 401.

OAuth credentials and tokens are scoped to the individual transport instance.

Transport Options

API key authentication

interface SendPulseApiKeyOptions {
  apiKey: string;
  baseUrl?: string;
}

OAuth authentication

interface SendPulseOAuthOptions {
  clientId: string;
  clientSecret: string;
  baseUrl?: string;
}

apiKey and OAuth credentials are mutually exclusive.

Custom API URL

The default SendPulse API URL is:

https://api.sendpulse.com

It can be overridden when necessary:

const transport = new SendPulseTransport({
  apiKey: process.env.SENDPULSE_API_KEY!,
  baseUrl: 'https://api.sendpulse.com',
});

Sending Email

All regular Nodemailer message options continue to work through sendMail().

await mailer.sendMail({
  from: {
    name: 'Acme',
    address: '[email protected]',
  },

  to: [
    {
      name: 'John',
      address: '[email protected]',
    },
    {
      name: 'Jane',
      address: '[email protected]',
    },
  ],

  cc: '[email protected]',

  bcc: '[email protected]',

  replyTo: {
    name: 'Acme Support',
    address: '[email protected]',
  },

  subject: 'Your account is ready',

  html: `
        <h1>Welcome!</h1>
        <p>Your account is ready.</p>
    `,

  text: 'Welcome! Your account is ready.',
});

HTML Emails

SendPulse expects HTML content to be Base64 encoded when using its SMTP API.

You do not need to handle this manually.

Pass normal HTML to Nodemailer:

await mailer.sendMail({
  from: '[email protected]',
  to: '[email protected]',
  subject: 'HTML email',

  html: `
        <h1>Hello</h1>
        <p>This will be encoded automatically.</p>
    `,
});

The transport handles the required encoding before sending the payload to SendPulse.

Attachments

Nodemailer attachments are supported.

await mailer.sendMail({
  from: '[email protected]',
  to: '[email protected]',

  subject: 'Your invoice',

  text: 'Your invoice is attached.',

  attachments: [
    {
      filename: 'invoice.pdf',
      content: invoiceBuffer,
    },
  ],
});

String content is also supported:

await mailer.sendMail({
  from: '[email protected]',
  to: '[email protected]',

  subject: 'Attachment example',

  text: 'See attachment.',

  attachments: [
    {
      filename: 'hello.txt',
      content: 'Hello from SendPulse.',
    },
  ],
});

Attachments are converted into the Base64 format expected by SendPulse.

Nodemailer-normalized attachments that are already Base64 encoded are preserved without being encoded a second time.

SendPulse Templates

SendPulse-specific options can be provided through the sendpulse property.

await mailer.sendMail({
  from: '[email protected]',
  to: '[email protected]',

  subject: 'Welcome',

  sendpulse: {
    template: {
      id: 12345,

      variables: {
        name: 'John',
        verificationCode: '482901',
      },
    },
  },
});

Template options:

interface SendPulseTemplate {
  id: string | number;
  variables: Record<string, unknown>;
}

When a SendPulse template is provided, the transport sends the template instead of the message's html and text fields.

Automatic Plain Text

SendPulse's auto_plain_text option can be enabled through the transport-specific message options:

await mailer.sendMail({
  from: '[email protected]',
  to: '[email protected]',

  subject: 'Hello',

  html: '<h1>Hello</h1>',

  sendpulse: {
    autoPlainText: true,
  },
});

By default:

autoPlainText: false;

Response

sendMail() follows Nodemailer's SentMessageInfo structure.

A successful response contains:

{
  (envelope, messageId, accepted, rejected, pending, response, sendpulseId);
}

Example:

const result = await mailer.sendMail({
  from: '[email protected]',
  to: '[email protected]',
  subject: 'Hello',
  text: 'Hello world',
});

console.log(result.messageId);
console.log(result.accepted);
console.log(result.response);

The transport additionally exposes the SendPulse message ID as:

result.sendpulseId;

The transport response follows this shape:

interface SendPulseTransportInfo extends SentMessageInfo {
  sendpulseId: string;
}

On a successful request:

result.response;

will contain a value similar to:

SendPulse queued message abc123

Accepted and rejected recipients

SendPulse's send endpoint does not provide per-recipient acceptance information when a message is initially queued.

When SendPulse successfully accepts the message:

accepted;

contains the recipients submitted through the message envelope, while:

rejected;

and:

pending;

remain empty.

Error Handling

SendPulse API errors are propagated through Nodemailer.

try {
  await mailer.sendMail({
    from: '[email protected]',
    to: '[email protected]',
    subject: 'Hello',
    text: 'Hello',
  });
} catch (error) {
  console.error(error);
}

HTTP errors include the SendPulse response status where available:

SendPulse API error (401): Unauthorized

or:

SendPulse API error (400): Sender email is not allowed

If SendPulse returns a successful HTTP response but reports that the message could not be sent, the provider's error message is thrown.

OAuth Token Handling

When OAuth authentication is used, access tokens are cached in memory.

This prevents every email from triggering a new authentication request.

For example:

const transport = new SendPulseTransport({
  clientId: process.env.SENDPULSE_CLIENT_ID!,
  clientSecret: process.env.SENDPULSE_CLIENT_SECRET!,
});

const mailer = nodemailer.createTransport(transport);

await mailer.sendMail({
  from: '[email protected]',
  to: '[email protected]',
  subject: 'First email',
  text: 'First',
});

await mailer.sendMail({
  from: '[email protected]',
  to: '[email protected]',
  subject: 'Second email',
  text: 'Second',
});

Both messages reuse the same OAuth token while it remains valid.

Concurrent messages

If several messages are sent simultaneously while a token is being requested, the transport shares the pending token request rather than requesting multiple tokens.

401 recovery

If SendPulse responds with 401 while using OAuth:

  1. The cached token is invalidated.
  2. A fresh OAuth token is requested.
  3. The original request is retried once.

A second authentication failure is returned immediately.

Static API keys are not retried after 401, because retrying the same invalid key would produce the same result.

Using Multiple Transport Instances

Each transport maintains its own authentication state.

const primary = new SendPulseTransport({
  apiKey: process.env.PRIMARY_SENDPULSE_API_KEY!,
});

const secondary = new SendPulseTransport({
  clientId: process.env.SECONDARY_SENDPULSE_CLIENT_ID!,
  clientSecret: process.env.SECONDARY_SENDPULSE_CLIENT_SECRET!,
});

Credentials and OAuth tokens are not shared between transport instances.

TypeScript

The package is designed for TypeScript and exposes the transport and SendPulse-specific message types.

import {
  SendPulseTransport,
  type SendPulseMailOptions,
  type SendPulseTransportInfo,
} from 'nodemailer-sendpulse-transport';

Example:

const message: SendPulseMailOptions = {
  from: '[email protected]',
  to: '[email protected]',

  subject: 'Hello',

  html: '<h1>Hello</h1>',

  sendpulse: {
    autoPlainText: true,
  },
};

Nodemailer return typing

Depending on the Nodemailer type definitions in use, nodemailer.createTransport() may expose the result as Nodemailer's standard SentMessageInfo rather than preserving provider-specific extensions.

If you need TypeScript access to sendpulseId, the transporter can be explicitly typed:

import nodemailer, { type Transporter } from 'nodemailer';

import {
  SendPulseTransport,
  type SendPulseTransportInfo,
} from 'nodemailer-sendpulse-transport';

const transport = new SendPulseTransport({
  apiKey: process.env.SENDPULSE_API_KEY!,
});

const mailer = nodemailer.createTransport(
  transport,
) as Transporter<SendPulseTransportInfo>;

Now:

const result = await mailer.sendMail({
  from: '[email protected]',
  to: '[email protected]',
  subject: 'Hello',
  text: 'Hello',
});

console.log(result.sendpulseId);

is correctly recognized by TypeScript.

API Key Example

import nodemailer from 'nodemailer';
import { SendPulseTransport } from 'nodemailer-sendpulse-transport';

const mailer = nodemailer.createTransport(
  new SendPulseTransport({
    apiKey: process.env.SENDPULSE_API_KEY!,
  }),
);

await mailer.sendMail({
  from: {
    name: 'My App',
    address: '[email protected]',
  },

  to: '[email protected]',

  subject: 'Welcome',

  html: `
        <h1>Welcome</h1>
        <p>Thanks for signing up.</p>
    `,

  text: 'Welcome. Thanks for signing up.',
});

OAuth Example

import nodemailer from 'nodemailer';
import { SendPulseTransport } from 'nodemailer-sendpulse-transport';

const mailer = nodemailer.createTransport(
  new SendPulseTransport({
    clientId: process.env.SENDPULSE_CLIENT_ID!,

    clientSecret: process.env.SENDPULSE_CLIENT_SECRET!,
  }),
);

await mailer.sendMail({
  from: {
    name: 'My App',
    address: '[email protected]',
  },

  to: '[email protected]',

  subject: 'Welcome',

  html: `
        <h1>Welcome</h1>
        <p>Thanks for signing up.</p>
    `,
});

Why an HTTP Transport?

Nodemailer commonly sends email through an SMTP connection.

This transport instead converts the Nodemailer message into the payload expected by SendPulse's transactional SMTP REST API.

Nodemailer
    ↓
SendPulseTransport
    ↓
SendPulse SMTP API
    ↓
Recipient

This means your application can continue using the standard Nodemailer API while authentication, payload conversion, Base64 encoding, and SendPulse-specific behavior are handled by the transport.

Requirements

  • Node.js with native fetch support

  • Nodemailer

  • A SendPulse account

  • Either:

    • a SendPulse API key, or
    • SendPulse OAuth client credentials
  • A sender permitted by your SendPulse SMTP account

Development and testing

The project can be tested with Vitest.

npm test

or:

pnpm test

The test suite should cover:

  • API-key authentication
  • OAuth authentication
  • OAuth token caching
  • OAuth token refresh
  • 401 retry behavior
  • Address mapping
  • cc, bcc, and replyTo
  • HTML encoding
  • Plain-text messages
  • Attachments
  • SendPulse templates
  • SendPulse API errors

Checks

pnpm lint
pnpm typecheck
pnpm build

License

MIT