Skip to main content

Node.js SDK

The official Node.js SDK gives you typed access to PostMX from Node.js 18+ with built-in retries, idempotent POST requests, and webhook signature verification.

Install

npm install postmx

Quickstart

import { PostMX } from "postmx";

async function main() {
const postmx = new PostMX(process.env.POSTMX_API_KEY!);

const inbox = await postmx.createInbox({
label: "signup-test",
lifecycle_mode: "temporary",
ttl_minutes: 15,
});

console.log("Send your app email to:", inbox.email_address);

const message = await postmx.waitForMessage(inbox.id, {
timeoutMs: 30_000,
intervalMs: 1_000,
});

console.log("OTP:", message.otp);
console.log("Intent:", message.intent);
}

main().catch(console.error);

Create the client

import { PostMX } from "postmx";

const postmx = new PostMX("pmx_live_...");

Constructor options

OptionTypeDefaultNotes
baseUrlstringhttps://api.postmx.coOverride for local or staged environments.
maxRetriesnumber2Retries apply to 429, 500, 502, 503, and 504.
timeoutnumber30000Request timeout in milliseconds.

Common workflows

Create an inbox

const inbox = await postmx.createInbox({
label: "checkout-flow",
lifecycle_mode: "temporary",
ttl_minutes: 15,
});

createInbox() accepts:

  • label: Human-readable name for the inbox.
  • lifecycle_mode: temporary or persistent.
  • ttl_minutes: Optional TTL for temporary inboxes. Current API limits are 10 to 60.

POST requests are safe to retry because the SDK automatically adds an Idempotency-Key if you do not pass one yourself.

List inboxes

const { inboxes, pageInfo, wildcard_address } = await postmx.listInboxes({
limit: 20,
});

listInboxes() returns:

  • inboxes: The current page of inboxes.
  • pageInfo: Pagination metadata with has_more and next_cursor.
  • wildcard_address: A wildcard address object when available, otherwise null.

List messages for an inbox

const { messages, pageInfo } = await postmx.listMessages(inbox.id, {
limit: 10,
});

Get a message

const message = await postmx.getMessage("msg_123");

console.log(message.subject);
console.log(message.text_body);
console.log(message.html_body);
console.log(message.otp);
console.log(message.links);

Fetch only the part you need

Use content_mode when you want smaller, focused responses.

const otpOnly = await postmx.getMessage("msg_123", "otp");
console.log(otpOnly.otp);

const linksOnly = await postmx.getMessage("msg_123", "links");
console.log(linksOnly.links);

const textOnly = await postmx.getMessage("msg_123", "text_only");
console.log(textOnly.text_body);

Supported modes:

  • full: Full message detail. This is the default.
  • otp: Message metadata plus otp.
  • links: Message metadata plus links.
  • text_only: Message metadata plus text_body.

Wait for a message

const message = await postmx.waitForMessage(inbox.id, {
intervalMs: 1_000,
timeoutMs: 60_000,
});

Notes:

  • intervalMs defaults to 1000.
  • timeoutMs defaults to 60000.
  • intervalMs must be at least 200.
  • waitForMessage() returns the newest message detail once an inbox has at least one message.

Create a webhook

const result = await postmx.createWebhook({
label: "production-events",
target_url: "https://example.com/webhooks/postmx",
});

console.log(result.webhook.id);
console.log(result.signing_secret);

You can also scope a webhook to one inbox:

await postmx.createWebhook({
label: "signup-only",
target_url: "https://example.com/webhooks/postmx",
inbox_id: inbox.id,
});

Use a final public HTTPS endpoint. The API rejects localhost targets, embedded credentials, and private or reserved IP literals. PostMX does not follow redirects, and each delivery attempt times out after 10 seconds.

Verify a webhook signature

import { verifyWebhookSignature } from "postmx";

const event = verifyWebhookSignature({
payload: rawBody,
signature:
(req.headers["x-postmx-signature"] as string) ??
(req.headers["postmx-signature"] as string),
timestamp: req.headers["x-postmx-timestamp"] as string,
signingSecret: process.env.POSTMX_WEBHOOK_SECRET!,
});

console.log(event.type);
console.log(event.data.message.otp);

Important:

  • Store the returned signing_secret immediately. It is returned once.
  • Pass the raw request body, not parsed JSON.
  • Read X-PostMX-Delivery-Id for attempt tracing and X-PostMX-Event-Id for diagnostics.
  • The signature format is v1=<base64url(hmac_sha256(signing_secret, timestamp + "." + raw_body))>.
  • The default timestamp tolerance is 300 seconds.
  • The payload already includes the full message plus extracted fields such as otp, links, and intent.

Error handling

import { PostMXApiError, PostMXNetworkError } from "postmx";

try {
await postmx.getMessage("msg_missing");
} catch (error) {
if (error instanceof PostMXApiError) {
console.log(error.status);
console.log(error.code);
console.log(error.message);
console.log(error.requestId);
console.log(error.retryAfterSeconds);
} else if (error instanceof PostMXNetworkError) {
console.log(error.cause.message);
}
}

Method reference

new PostMX(apiKey, options?)
postmx.listInboxes(params?)
postmx.createInbox(params, options?)
postmx.listMessages(inboxId, params?)
postmx.getMessage(messageId, contentMode?)
postmx.createWebhook(params, options?)
postmx.waitForMessage(inboxId, options?)

Good defaults

  • Use temporary inboxes with a short TTL in automated tests.
  • Use postmx.getMessage(messageId, "otp") or postmx.getMessage(messageId, "links") when you only need one extracted field.
  • Keep maxRetries enabled unless you already have higher-level retry logic.
  • Store the webhook signing_secret when a webhook is created. It is the value you need for signature verification later.