SMTP
Send through smtp.postfly.app on port 587 with STARTTLS and your API key — for apps and frameworks that speak SMTP.
For apps and frameworks that speak SMTP rather than HTTP. A message submitted over SMTP goes through the same pipeline as POST /v1/emails: the sender domain must be verified, suppressed recipients are refused, and each recipient becomes one message you can follow in the dashboard, the API and webhooks.
Connection settings
| Host | smtp.postfly.app |
|---|---|
| Port | 587 (STARTTLS) |
| Username | smtp-…@yourdomain.com, shown as SMTP username beside each key on a domain's Keys & SMTP tab. It names the key and the domain you send from: one key has one username per domain it may send from, and a username that names another key or a domain the key may not send from is refused. While the domain is still pending, a correct key and username get 535 5.7.8 Sender domain not verified yet; every other refused login gets the same generic 535. apikey is still accepted. |
| Password | your API key, pf_live_… or pf_test_…, with the emails:send scope |
| Encryption | STARTTLS is required; AUTH is refused on an unencrypted session. |
How a message is read
- The
From:header is the sender; its domain must be a verified sender domain of the key may send from. The envelopeMAIL FROMis used only as the bounce address. - Each
RCPT TOaddress becomes one message, up to 50 per SMTP message; a message with more is refused. To:/Cc:are kept as submitted, display names included, identically on every recipient's copy; an address that was only on the envelope and in neither header (a Bcc) is never added to them. Each recipient still counts as its own message toward the daily quota.Subject:, the HTML and text parts andReply-To:(with its display name) are taken as they are; a Reply-To with several addresses is refused. A message with neither an HTML nor a text part is refused.X-Postfly-Tag:sets the message'stag. Other custom headers are dropped; postfly builds the outgoing headers itself.- Attachments are carried as they are: at most 10, each up to 5 MiB, of the types application/pdf, application/xml, text/xml, text/plain, text/csv, image/png, image/jpeg, application/zip, application/x-isdoc, application/x-isdocx. A message with anything postfly can't carry is refused as a whole rather than sent without it: another type (calendar invitations and forwarded messages included), an executable file extension, a file name that doesn't match its type, or an image embedded in the HTML (
cid:). Every recipient gets its own copy: together they may hold at most 20 MiB of attachments. - A message may be up to 10 MiB.
Replies & limits
A refusal comes back as an SMTP reply. Validation, domain and suppression problems are permanent 550s. Paused sending gets 550 5.7.1 at MAIL FROM. Attachments over a limit get 552 5.3.4, ones postfly doesn't carry 550 5.6.1, a blocked file extension 550 5.7.1. Temporary server faults are 451, safe to retry.
The rate limit is the REST default bucket, 120 per minute per key, counted per recipient, with its own counter separate from the REST API. Past it, further recipients get 452 and a new message gets 451 at MAIL FROM, both saying when to retry; standard clients keep the accepted recipients and retry the rest. The daily quota applies too: a message whose recipients would pass it gets 452 after DATA and none of them is queued. The full table is in SMTP replies.
Examples
import nodemailer from 'nodemailer';
const transport = nodemailer.createTransport({
host: 'smtp.postfly.app',
port: 587, // STARTTLS
requireTLS: true,
auth: {
user: process.env.POSTFLY_SMTP_USER, // smtp-…@yourdomain.com
pass: process.env.POSTFLY_API_KEY,
},
});
await transport.sendMail({
from: 'Your App <noreply@yourdomain.com>',
to: 'user@example.com',
subject: 'Your sign-in code',
text: 'Your code is 381902.',
html: '<p>Your code is <strong>381902</strong>.</p>',
headers: { 'X-Postfly-Tag': 'sign-in' },
});