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
| Option | Type | Default | Notes |
|---|---|---|---|
baseUrl | string | https://api.postmx.co | Override for local or staged environments. |
maxRetries | number | 2 | Retries apply to 429, 500, 502, 503, and 504. |
timeout | number | 30000 | Request 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:temporaryorpersistent.ttl_minutes: Optional TTL for temporary inboxes. Current API limits are10to60.
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 withhas_moreandnext_cursor.wildcard_address: A wildcard address object when available, otherwisenull.
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 plusotp.links: Message metadata pluslinks.text_only: Message metadata plustext_body.
Wait for a message
const message = await postmx.waitForMessage(inbox.id, {
intervalMs: 1_000,
timeoutMs: 60_000,
});
Notes:
intervalMsdefaults to1000.timeoutMsdefaults to60000.intervalMsmust be at least200.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_secretimmediately. It is returned once. - Pass the raw request body, not parsed JSON.
- Read
X-PostMX-Delivery-Idfor attempt tracing andX-PostMX-Event-Idfor diagnostics. - The signature format is
v1=<base64url(hmac_sha256(signing_secret, timestamp + "." + raw_body))>. - The default timestamp tolerance is
300seconds. - The payload already includes the full message plus extracted fields such as
otp,links, andintent.
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
temporaryinboxes with a short TTL in automated tests. - Use
postmx.getMessage(messageId, "otp")orpostmx.getMessage(messageId, "links")when you only need one extracted field. - Keep
maxRetriesenabled unless you already have higher-level retry logic. - Store the webhook
signing_secretwhen a webhook is created. It is the value you need for signature verification later.