Node.js SDK
The mailpush package wraps the API in a typed client. It has no dependency: it uses the runtime’s fetch and Web Crypto.
Install
Section titled “Install”npm install mailpushIt needs Node.js 22 or later, Bun, Deno, or an edge runtime with fetch.
Create the client
Section titled “Create the client”import { MailPush } from "mailpush";
const mailpush = new MailPush(process.env.MAILPUSH_API_KEY);| Option | Default | Description |
|---|---|---|
baseUrl |
https://api.mailpush.app |
Where requests go. |
maxRetries |
2 |
Retries after a network error, a 5xx answer or rate_limited. |
timeoutMs |
30000 |
Time allowed for each attempt. |
fetch |
The global fetch |
A replacement, for tests or a proxy. |
mailpush.testMode is true when the key starts with mp_test_.
Send an email
Section titled “Send an email”const { id } = await mailpush.emails.send({ from: "Acme <hello@example.com>", to: ["customer@example.org", "billing@example.org"], replyTo: "support@example.com", subject: "Your order is ready", html: "<p>Hello, your order is waiting for you.</p>",});The fields are those of POST /v1/emails, in camelCase: reply_to becomes replyTo.
Each call carries its own Idempotency-Key, which the SDK reuses when it retries, so a retry never sends twice. To also cover the retries of your own code, pass a key that identifies the email:
await mailpush.emails.send(email, { idempotencyKey: `order-${order.id}-confirmation` });Send with a template
Section titled “Send with a template”await mailpush.emails.send({ from: "Acme <hello@example.com>", to: "customer@example.org", template: "order-confirmation", data: { firstName: "Camille", order: { id: "1234" } },});template is the alias or the id of a published template, and data holds the values of its variables. subject is optional and replaces the subject of the template. TypeScript refuses html or text next to template. This needs version 0.3.0 or later of the package.
Read an email
Section titled “Read an email”const email = await mailpush.emails.get(id);
for (const recipient of email.recipients) { console.log(recipient.email, recipient.status, recipient.lastResponse);}Dates such as createdAt, deliveredAt, openedAt and clickedAt are Date objects. openedAt and clickedAt need version 0.4.0 or later, like the tracking option of send: see Track opens and clicks.
Handle errors
Section titled “Handle errors”Every failure is a MailPushError. Test its code, not its message.
import { MailPushError } from "mailpush";
try { await mailpush.emails.send(email);} catch (error) { if (error instanceof MailPushError && error.code === "daily_quota_exceeded") { console.log(`Quota reached, retry in ${error.retryAfter} seconds`); } else { throw error; }}| Property | Description |
|---|---|
code |
An API error code, or network_error, timeout, invalid_signature. |
status |
The HTTP status, when the API answered. |
retryAfter |
Seconds to wait, when the API sent Retry-After. |
The SDK never retries daily_quota_exceeded or a 4xx other than rate_limited.
Verify a webhook
Section titled “Verify a webhook”const event = await mailpush.webhooks.verify(request, process.env.MAILPUSH_WEBHOOK_SECRET);
if (event.type === "email.bounced") { console.log(event.data.recipient, event.data.bounce.type);}request is the standard Request your handler receives. With Express or plain Node.js, pass the raw body and the headers, and import the function directly:
import { verifyWebhook } from "mailpush";
const event = await verifyWebhook({ payload: rawBody, headers: req.headers }, secret);verify throws a MailPushError with the code invalid_signature when the signature does not match or the request is older than 5 minutes. Change the window with { toleranceSeconds: 600 } as a third argument. See Receive webhooks for the full flow.