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, andreplyTo- Attachments
- SendPulse templates
- Nodemailer-compatible
SentMessageInfo
While nodemailer-sendpulse-transport is completely framework agnostic, it is fully supported and can be used in:
- Arcstack
- H3ravel
npm install nodemailer-sendpulse-transport nodemailerUsing pnpm:
pnpm add nodemailer-sendpulse-transport nodemailerUsing Yarn:
yarn add nodemailer-sendpulse-transport nodemailerCreate 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: 'hello@example.com',
},
to: 'user@example.com',
subject: 'Hello from SendPulse',
html: `
<h1>Hello</h1>
<p>This email was sent through SendPulse.</p>
`,
text: 'Hello. This email was sent through SendPulse.',
});SendPulse supports both static API keys and OAuth client credentials.
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.
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:
- Requests an access token from SendPulse.
- Caches the token in memory.
- Reuses the token for subsequent messages.
- Refreshes the token before its expiration.
- Requests a fresh token and retries once when SendPulse unexpectedly responds with
401.
OAuth credentials and tokens are scoped to the individual transport instance.
interface SendPulseApiKeyOptions {
apiKey: string;
baseUrl?: string;
}interface SendPulseOAuthOptions {
clientId: string;
clientSecret: string;
baseUrl?: string;
}apiKey and OAuth credentials are mutually exclusive.
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',
});All regular Nodemailer message options continue to work through sendMail().
await mailer.sendMail({
from: {
name: 'Acme',
address: 'hello@example.com',
},
to: [
{
name: 'John',
address: 'john@example.com',
},
{
name: 'Jane',
address: 'jane@example.com',
},
],
cc: 'manager@example.com',
bcc: 'audit@example.com',
replyTo: {
name: 'Acme Support',
address: 'support@example.com',
},
subject: 'Your account is ready',
html: `
<h1>Welcome!</h1>
<p>Your account is ready.</p>
`,
text: 'Welcome! Your account is ready.',
});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: 'hello@example.com',
to: 'user@example.com',
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.
Nodemailer attachments are supported.
await mailer.sendMail({
from: 'hello@example.com',
to: 'user@example.com',
subject: 'Your invoice',
text: 'Your invoice is attached.',
attachments: [
{
filename: 'invoice.pdf',
content: invoiceBuffer,
},
],
});String content is also supported:
await mailer.sendMail({
from: 'hello@example.com',
to: 'user@example.com',
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-specific options can be provided through the sendpulse property.
await mailer.sendMail({
from: 'hello@example.com',
to: 'user@example.com',
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.
SendPulse's auto_plain_text option can be enabled through the transport-specific message options:
await mailer.sendMail({
from: 'hello@example.com',
to: 'user@example.com',
subject: 'Hello',
html: '<h1>Hello</h1>',
sendpulse: {
autoPlainText: true,
},
});By default:
autoPlainText: false;sendMail() follows Nodemailer's SentMessageInfo structure.
A successful response contains:
{
(envelope, messageId, accepted, rejected, pending, response, sendpulseId);
}Example:
const result = await mailer.sendMail({
from: 'hello@example.com',
to: 'user@example.com',
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
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.
SendPulse API errors are propagated through Nodemailer.
try {
await mailer.sendMail({
from: 'hello@example.com',
to: 'user@example.com',
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.
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: 'hello@example.com',
to: 'one@example.com',
subject: 'First email',
text: 'First',
});
await mailer.sendMail({
from: 'hello@example.com',
to: 'two@example.com',
subject: 'Second email',
text: 'Second',
});Both messages reuse the same OAuth token while it remains valid.
If several messages are sent simultaneously while a token is being requested, the transport shares the pending token request rather than requesting multiple tokens.
If SendPulse responds with 401 while using OAuth:
- The cached token is invalidated.
- A fresh OAuth token is requested.
- 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.
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.
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: 'hello@example.com',
to: 'user@example.com',
subject: 'Hello',
html: '<h1>Hello</h1>',
sendpulse: {
autoPlainText: true,
},
};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: 'hello@example.com',
to: 'user@example.com',
subject: 'Hello',
text: 'Hello',
});
console.log(result.sendpulseId);is correctly recognized by TypeScript.
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: 'hello@example.com',
},
to: 'user@example.com',
subject: 'Welcome',
html: `
<h1>Welcome</h1>
<p>Thanks for signing up.</p>
`,
text: 'Welcome. Thanks for signing up.',
});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: 'hello@example.com',
},
to: 'user@example.com',
subject: 'Welcome',
html: `
<h1>Welcome</h1>
<p>Thanks for signing up.</p>
`,
});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.
-
Node.js with native
fetchsupport -
Nodemailer
-
A SendPulse account
-
Either:
- a SendPulse API key, or
- SendPulse OAuth client credentials
-
A sender permitted by your SendPulse SMTP account
The project can be tested with Vitest.
npm testor:
pnpm testThe test suite should cover:
- API-key authentication
- OAuth authentication
- OAuth token caching
- OAuth token refresh
401retry behavior- Address mapping
cc,bcc, andreplyTo- HTML encoding
- Plain-text messages
- Attachments
- SendPulse templates
- SendPulse API errors
pnpm lint
pnpm typecheck
pnpm build