Sign in Create account
Guides

SMTP relay: settings and examples

Send through Postal from any system that speaks SMTP, using an API key as the password.

#Configuration

For systems that cannot talk to APIs (WordPress, ERPs, store platforms, printers), use Postal's SMTP server. Sends go through the same rules as the API: verified domain, quotas, suppression list, and they appear under Emails and Logs. SMTP is part of the plan (smtp_access): without it, the message is refused with 550 5.7.1.

FieldValue
Serversmtp.postal.so
Port465 (implicit TLS, recommended) or 587 (STARTTLS). If your network blocks those, use 2465 (TLS) or 2587 (STARTTLS).
Usernamepostal
PasswordAn API key (ps_…). A sending-only key is enough; create one just for SMTP.
AuthenticationAUTH PLAIN or AUTH LOGIN, always after TLS.

The recipients of emails sent over SMTP (To and Cc) are kept in the Audience as unconfirmed contacts, who get no marketing until you confirm: see Automatic audience from transactional emails.

#Examples

SMTP

Node.js

import nodemailer from 'nodemailer';

const transporter = nodemailer.createTransport({
  host: 'smtp.postal.so',
  port: 465,
  secure: true, // implicit TLS; for port 587 use secure: false
  auth: {
    user: 'postal',
    pass: process.env.POSTAL_API_KEY,
  },
});

const info = await transporter.sendMail({
  from: 'Acme Store <[email protected]>',
  to: '[email protected]',
  subject: 'Your order #10482 is out for delivery',
  html: '<p>Order <b>#10482</b> is on its way.</p>',
});

console.log(info.response); // 250 2.0.0 OK queued as 019a1b2c-...

Python

import os
import smtplib
from email.message import EmailMessage

message = EmailMessage()
message["From"] = "Acme Store <[email protected]>"
message["To"] = "[email protected]"
message["Subject"] = "Your order #10482 is out for delivery"
message.set_content("Order #10482 is on its way.")
message.add_alternative("<p>Order <b>#10482</b> is on its way.</p>", subtype="html")

with smtplib.SMTP_SSL("smtp.postal.so", 465) as server:
    server.login("postal", os.environ["POSTAL_API_KEY"])
    server.send_message(message)

PHP

use PHPMailer\PHPMailer\PHPMailer;

$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host = 'smtp.postal.so';
$mail->Port = 465;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; // 587: ENCRYPTION_STARTTLS
$mail->SMTPAuth = true;
$mail->Username = 'postal';
$mail->Password = getenv('POSTAL_API_KEY');
$mail->CharSet = 'UTF-8';

$mail->setFrom('[email protected]', 'Acme Store');
$mail->addAddress('[email protected]');
$mail->Subject = 'Your order #10482 is out for delivery';
$mail->isHTML(true);
$mail->Body = '<p>Order <b>#10482</b> is on its way.</p>';
$mail->send();

Laravel

# .env
MAIL_MAILER=smtp
MAIL_SCHEME=smtps
MAIL_HOST=smtp.postal.so
MAIL_PORT=465
MAIL_USERNAME=postal
MAIL_PASSWORD=ps_your_key
[email protected]
MAIL_FROM_NAME="Acme Store"

cURL

curl --ssl-reqd --url "smtps://smtp.postal.so:465" \
  --user "postal:$POSTAL_API_KEY" \
  --mail-from "[email protected]" \
  --mail-rcpt "[email protected]" \
  --upload-file message.eml

#Laravel

With the .env above, Mail::to($customer)->send(new OrderShipped($order)) goes out through Postal without changing your code. On port 587, use MAIL_SCHEME=smtp and Laravel enables STARTTLS on its own.

#One-time codes (OTP-Token)

Postal delivers the message as it arrived, so the OTP-Token header ("123456"; origin="https://app.acmestore.com") reaches the recipient the way your system wrote it. The thing to watch is your SMTP library, which must not encode the value (RFC 2047) or swap the quotes. In Nodemailer, for example, use { prepared: true, value }; in PHPMailer, values longer than 47 characters need a subclass; in Laravel, the Mailable's headers() is enough. The format, examples for each library and how to read the code on the receiving side are in the OTP-Token header guide.

#How messages are handled

  • The sender comes from the message's From header, which must be on a verified domain (and on the key's domain, if the key is restricted).
  • Recipients are the envelope ones (RCPT TO). Anyone who appears in neither To nor Cc is treated as Bcc.
  • The message is sent as it arrived, with attachments and inline images. It stays available at GET /emails/{id}/raw.
  • Custom headers (X-…, List-Unsubscribe, OTP-Token, Reply-To…) go through exactly as your system wrote them. The only changes are that Bcc is removed (whoever is only in it receives through the envelope and is not shown to the others) and that an OTP-Token that arrives as RFC 2047 encoded words (Nodemailer, PHPMailer) is decoded and unfolded by Postal, which delivers the original value (see the OTP-Token guide).
  • SMTP has no tags, scheduling or templates: use the API for those. Quotas, the suppression list, events and webhooks are the same.
  • The test addresses ([email protected]…) work too: the email is simulated, uses no quota and generates the real events and webhooks. Do not mix test addresses with real recipients in the same message (550 5.6.0).
  • When it accepts a message, the server answers 250 2.0.0 OK queued as <id>: the ID is the email's ID in the API.
  • Each message appears in the logs with the SMTP method (filter with source=smtp).
  • Limits: 40 MB per message and 50 recipients. Idle sessions close after 5 minutes.

#Error responses

CodeMeaning
535 5.7.8Wrong username or password. The username is postal and the password is the API key. After 3 failures the connection closes; many failures from the same IP block new attempts for 10 minutes.
530 5.7.0Authentication is missing, or you tried to authenticate before STARTTLS on port 587.
550 5.7.1Send refused: unverified domain or one paused by the plan, key restricted to another domain, sending paused (sending_paused) or a feature the plan does not include (SMTP, test addresses, attachments). The message states the reason.
550 5.6.0Invalid message (for example, no valid From, a malformed address, or test addresses mixed with real recipients).
452 4.5.3Daily or monthly quota reached, or more than 50 recipients. It is temporary: the client can try again later.
552 5.3.4Message larger than 40 MB.
451 4.3.0Temporary failure. Try again.