MailKite
Get your API key
All server guides
Open Source Mail Servers

Send email with the Postal API from Node.js

Call Postal's HTTP API from Node.js, handle its JSON result safely, and compare the same application send with MailKite's SDK.

Reviewed

Postal’s HTTP API sends email by accepting a JSON POST authenticated with an X-Server-API-Key header. In Node.js, keep that credential on the server, check both the HTTP response and Postal’s JSON status, and record the returned message identifiers. Postal is self-hosted, so the API call does not remove responsibility for DNS, delivery reputation, server maintenance, or bounce handling.

Before you send

Postal logo

You need a running Postal server, a virtual mail server, a verified sending domain, and an API credential created for that mail server. Use the API URL and key from your own Postal installation. Never put a server API key in frontend JavaScript, a mobile app, a public repository, or a log.

Postal’s API documentation describes a v1 API and notes that it does not manage every Postal function. Check its current API reference before depending on an endpoint. This example uses the documented send-message endpoint.

Send one message from Node.js

Set the server URL and credential in your deployment’s secret manager:

export POSTAL_URL="https://postal.mail.example.net"
export POSTAL_API_KEY="replace-with-a-server-credential"

Then send a small controlled test message. The request and response shape follow Postal’s send API documentation:

const postalUrl = process.env.POSTAL_URL;
const apiKey = process.env.POSTAL_API_KEY;

if (!postalUrl || !apiKey) {
  throw new Error("Set POSTAL_URL and POSTAL_API_KEY");
}

const response = await fetch(`${postalUrl}/api/v1/send/message`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Server-API-Key": apiKey,
  },
  body: JSON.stringify({
    to: ["test-recipient@your-controlled-domain.example"],
    from: "notifications@your-sending-domain.example",
    subject: "Postal API test",
    plain_body: "This is a controlled test message.",
  }),
});

const result = await response.json();
if (!response.ok || result.status !== "success") {
  throw new Error(`Postal send failed: ${JSON.stringify(result)}`);
}

console.log({
  messageId: result.data?.message_id,
  recipients: result.data?.messages,
});

Replace both example addresses with addresses you control before running this request. Postal’s general API response contains a status field with success or error; do not treat HTTP success alone as application success. For a multi-recipient request, the result includes message identifiers and tokens per recipient. Store those IDs so that later delivery events can be correlated.

For real application code, validate the request body before calling Postal, set an explicit timeout, and avoid logging message content or credentials. Do not blindly retry an ambiguous timeout: Postal may have accepted the message before the connection failed. Persist an application send record first and reconcile it against Postal’s message history/webhooks before deciding whether a retry is safe.

Track delivery separately from API acceptance

An accepted API request is not the same as a delivered message. Configure Postal’s message status and bounce webhooks and process each event idempotently. Persist the provider message ID, recipient, event type, and event timestamp. Bounces and delayed delivery should update application state; they should not silently trigger an unbounded resend loop.

Test your handler with duplicate events and out-of-order events. Postal’s webhook payloads include message details, but the exact available fields vary by event type. Consult the official examples and retrieve additional details through the API only when necessary.

The equivalent send with MailKite

If your application uses MailKite for sending, the server SDK provides a different operational boundary: MailKite hosts the delivery service, while your app uses its API key. The equivalent Node.js SDK call is:

import { MailKite } from "mailkite";

const mk = new MailKite(process.env.MAILKITE_API_KEY);
const sent = await mk.send({
  from: "notifications@your-verified-domain.example",
  to: "test-recipient@your-controlled-domain.example",
  subject: "MailKite API test",
  text: "This is a controlled test message.",
});

console.log({ id: sent.id });

Use the MailKite sending docs for the current SDK request and response contract. MailKite is not a Postal API-compatible drop-in: credentials, endpoints, response types, domain verification, and operational ownership differ. Choose one delivery path for a given message rather than sending through both during a cutover.

When to use each path

Choose Postal’s API whenChoose MailKite’s SDK when
You want to operate the delivery platform and own its host, database, IP, and DNSYou want a hosted sending API and do not want to run the mail server
You need Postal’s server controls, message history, and API behaviorYour app benefits from MailKite’s send API alongside inbound webhook workflows
Your team can manage upgrades, reputation, monitoring, and bounce processingYour team prefers managed delivery operations and the current MailKite product contract fits

To receive mail as well, see receive inbound email with Postal or the MailKite receiving docs. The Postal for AI agents article compares the broader receive-and-reply architecture.

Sources