Node.js SDK
The Node.js SDK is easiest to use with one mental model: create a temporary inbox, wait for the next email, then read the extracted field you need.
Install
npm install postmx
Super Simple Path
import { PostMX } from "postmx";
async function main() {
const postmx = new PostMX(process.env.POSTMX_API_KEY!);
const inbox = await postmx.createTemporaryInbox({
label: "signup-test",
});
console.log("Send your app email to:", inbox.email_address);
const message = await postmx.waitForMessage(inbox.id, {
timeoutMs: 30_000,
});
console.log("OTP:", message.otp);
console.log("First link:", message.links[0]?.url ?? null);
}
main().catch(console.error);
waitForMessage() returns the latest existing message immediately if the inbox already has one; otherwise it waits for the next incoming email until the timeout.
What Do You Want Back?
The default message already includes extracted fields like otp, links, and intent.
If you already have a message ID, contentMode is just a simple "what do you want back?" choice:
const otpOnly = await postmx.getMessage("msg_123", "otp");
console.log(otpOnly.otp);
const linksOnly = await postmx.getMessage("msg_123", "links");
console.log(linksOnly.links);
Supported modes:
full: full message detailotp: message metadata plusotplinks: message metadata pluslinkstext_only: message metadata plustext_body
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. |
Advanced
Lifecycle controls
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 and wildcard addresses
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,
});
List messages for a recipient email
const { messages, pageInfo } = await postmx.listMessagesByRecipient(
"[email protected]",
{ 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);
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 latest existing message immediately if the inbox already has one; otherwise it waits for the next incoming email.
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.createTemporaryInbox(params, options?)
postmx.listMessages(inboxId, params?)
postmx.listMessagesByRecipient(recipientEmail, params?)
postmx.getMessage(messageId, contentMode?)
postmx.createWebhook(params, options?)
postmx.waitForMessage(inboxId, options?)
Good defaults
- Use
createTemporaryInbox()for the beginner path, then drop down tocreateInbox()when you need lifecycle controls. - 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.