Skip to content

SMTP relay

Send through PostStack from anything that speaks SMTP — a framework mailer, a CMS, a mail server — with the same tracking, suppression, webhooks and logs as the REST API.

Connection settings#

These are the only values you need. The same settings are on the API keys page of the dashboard, under SMTP configuration.

SettingValue
Hostsmtp.poststack.dev
Port587 (STARTTLS or implicit TLS) or 465 (implicit TLS)
EncryptionRequired. TLS 1.2 or 1.3, either from the first byte or via STARTTLS before you authenticate
AuthenticationSMTP AUTH PLAIN or LOGIN
Usernamepoststack — any value is accepted when the password is an API key
PasswordAn API key, sk_live_… (or sk_test_… to simulate — see below)
Maximum message size36 MB (SIZE 37748736 in the EHLO reply)

Use a live key to deliver mail

An sk_test_… key authenticates and gets a 250, but the message is simulated: it is recorded and marked delivered without ever being handed to a mail server, and nothing reaches the recipient. Because the SMTP reply is the only thing an SMTP client ever sees, the reply says so: 250 Accepted em_… in TEST MODE - simulated only, nothing was delivered. A test send does not count toward your monthly quota. See Test mode.

A mailbox can also sign in here with its full address and mailbox password instead of an API key — that is how Thunderbird, Apple Mail and Outlook send. The differences are listed under Mailbox credentials.

Ports and TLS#

Both ports land on the same relay; they differ only in how TLS starts. The relay looks at the first byte your client sends: a TLS ClientHello means implicit TLS, and a client that sends nothing and waits for a greeting is treated as STARTTLS.

PortHow TLS startsUse it when
587STARTTLS (plaintext greeting, then upgrade) — or implicit TLS on connectAlmost always. Most mailers default to STARTTLS on 587.
465Implicit TLS on connect (RFC 8314 “submissions”)Your client has an “SSL/TLS” or “secure: true” switch rather than STARTTLS.
25—Never for sending. Port 25 is the inbound MX (inbound.poststack.dev): it does not accept AUTH and refuses to relay to outside domains.

On a plaintext connection to 587 the relay answers only EHLO, HELO, STARTTLS and QUIT. Any other command — AUTH, MAIL FROM — is answered with 530 5.7.0 Must issue STARTTLS first, so a password can never cross the wire unencrypted. There is no unencrypted port and no port 2525 for submission.

The certificate is a Let's Encrypt certificate for poststack.dev that also names smtp.poststack.dev, so verification succeeds with the host above. After TLS the relay advertises AUTH PLAIN LOGIN, SIZE 37748736, 8BITMIME, SMTPUTF8 and PIPELINING.

Send your first message#

  1. Verify the domain you send from

    The address in From must be on a domain that is verified on your team. A message from any other domain is refused at the end of DATA with 550 Domain 'example.com' not found for this team or … is not verified. See Domains.

  2. Create an API key

    Any key works — Full access or Sending only. A key restricted to one domain can only send from that domain; anything else gets 550 API key is restricted to a different domain. See Authentication.

  3. Point your mailer at the relay

    import nodemailer from 'nodemailer';
    
    const transporter = nodemailer.createTransport({
      host: 'smtp.poststack.dev',
      port: 587,
      secure: false,      // start in plaintext…
      requireTLS: true,   // …and refuse to continue unless STARTTLS succeeds
      auth: {
        user: 'poststack',                     // any value when the password is an API key
        pass: process.env.POSTSTACK_API_KEY,   // sk_live_…
      },
    });
    // Implicit TLS instead: port: 465, secure: true (drop requireTLS).
    
    const info = await transporter.sendMail({
      from: 'Acme <hello@yourdomain.com>',
      to: 'user@example.com',
      subject: 'Welcome to Acme',
      html: '<p>Thanks for signing up.</p>',
      text: 'Thanks for signing up.',
      headers: {
        'X-PostStack-Tags': 'welcome,onboarding',
      },
    });
    console.log(info.response); // "250 OK: message queued"
  4. Find it in the dashboard

    A live send is answered 250 OK: message queued; the email id is not part of that reply. The message appears under Emails within a second, with the same statuses and webhook events as an API send. Tag it with X-PostStack-Tags to find it again.

Framework settings#

Every framework mailer needs the same four values. These use port 587 with STARTTLS and read the key from an environment variable.

# settings.py
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.poststack.dev"
EMAIL_PORT = 587
EMAIL_USE_TLS = True          # STARTTLS. For port 465 use EMAIL_USE_SSL = True instead.
EMAIL_HOST_USER = "poststack"
EMAIL_HOST_PASSWORD = os.environ["POSTSTACK_API_KEY"]
DEFAULT_FROM_EMAIL = "Acme <hello@yourdomain.com>"

Step-by-step guides for these and other stacks are under Framework guides — for example Django, Laravel and Ruby.

PostStack headers#

Add these headers to a message to use API features over SMTP. They are read as instructions and removed; the recipient never sees them. Header names are case-insensitive.

X-PostStack-Tagsstring
Comma-separated tags, e.g. welcome,onboarding. Whitespace around each tag is trimmed and empty entries are dropped. Same as the API's tags.
X-PostStack-Idempotency-Keystring
A key that makes a retry safe. If your team has already sent a message with this key, the relay answers 250 and sends nothing new. Over SMTP a repeated key is always treated as a replay, even if the message content changed — unlike the HTTP API, which answers 422 for a different payload. Same as idempotency_key.
X-PostStack-Template-Idstring
The tpl_… id of a published template. Its subject, HTML and text replace the ones in your message. An unknown id is refused with 550 Template not found, an unpublished one with 550 Template is not published. See Templates.
X-PostStack-Variablesstring (JSON object)
The template variables as one line of JSON, e.g. {"first_name":"Alice"}. JSON that does not parse is ignored without an error, and the template renders with no variables — check the rendered email if a placeholder comes out empty.
X-PostStack-Schedulestring (ISO 8601)
Deliver later instead of now, e.g. 2026-10-01T09:00:00Z. Include a timezone offset or Z. Same as scheduled_at.
typescript
await transporter.sendMail({
  from: 'Acme <hello@yourdomain.com>',
  to: 'user@example.com',
  // subject, html and text are replaced by the rendered template
  headers: {
    'X-PostStack-Template-Id': 'tpl_abc123def456ghi789',
    'X-PostStack-Variables': JSON.stringify({ first_name: 'Alice', company_name: 'Acme' }),
    'X-PostStack-Idempotency-Key': 'welcome-user-4821',
    'X-PostStack-Schedule': '2026-10-01T09:00:00Z',
  },
});

The HTTP API validates these values before accepting a send (tags at most 10 × 64 characters, idempotency keys at most 64). SMTP submission does not go through that validation, so keep to the same limits to get the same result on both paths.

How your message is processed#

PostStack is a submission relay, not a byte-for-byte forwarder. Your message is parsed into its parts — addresses, subject, HTML and text bodies, attachments — recorded as an email you can see in the dashboard, and composed again on the way out. That is what makes tracking, templates, suppression and webhooks work over SMTP, and it has these consequences:

  • Recipients. To and Cc come from the message headers. Any envelope recipient (RCPT TO) that is in neither becomes a Bcc. If the message has no To header, the envelope recipients are used. The sender is the From header, display name included.
  • Your DKIM signature is replaced. If your server already signs (Mailcow, Postfix with OpenDKIM, Microsoft 365), that signature cannot survive recomposition — it covers the exact bytes. PostStack signs the outgoing message with the key it holds for your verified domain, so it still leaves with a valid d=yourdomain.com signature that aligns for DMARC. There is no passthrough mode.
  • Only some headers are carried. Headers starting with X- (other than the PostStack ones) are passed through, and In-Reply-To and References are kept so replies thread. A very long References chain is trimmed to 1,024 characters, keeping the first id and the most recent ones. Every other header is dropped, and PostStack sets its own Message-ID and Date.
  • Tracking changes your HTML. Open tracking adds a pixel and click tracking rewrites links. Both are on by default and can be turned off per domain — see Tracking.
  • Inline images stay inline. Parts referenced from the HTML with cid: are kept as inline attachments, not converted to data: URIs (which Gmail and Outlook strip).
  • No mailbox signature. A mailbox signature set in PostStack is never added to SMTP submissions, because your mail client has already added its own.

Limits#

LimitValueWhat happens past it
Message size36 MB raw (after MIME encoding)Refused with 552 during DATA
Attachment size10 MB per file, 25 MB per message, measured decoded550 naming the file and the limit
Recipients50 per message, across To, Cc and Bcc550 Too many recipients: …
Monthly email quotaYour plan’s allowance451 Monthly email limit reached. Upgrade your plan. (temporary, so your server keeps the message and retries)
Failed logins10 per 15 minutes per IP address or usernameFurther attempts get 535 Too many failed attempts. Try again later. until the window passes
Mailbox sending rate60 messages per hour per mailbox (mailbox credentials only)450 Rate limit exceeded. Try again later.

Attachments are counted after decoding, so a 10 MB file is 10 MB here even though it is about 13.5 MB inside the message. API-key sends have no per-connection rate limit beyond your plan quota.

Replies and error codes#

Authentication is checked at AUTH; everything else — sender domain, template, quota, suppression — is checked once the whole message has arrived, and the answer is the reply to the final . of DATA. A 5xx is permanent: fix the message rather than retrying. A 4xx is temporary and a mail server will retry it on its own.

ReplyMeaning
250 OK: message queuedAccepted for delivery (or scheduled).
250 Accepted em_… in TEST MODE …Accepted with a test key. Nothing is delivered.
530 5.7.0 Must issue STARTTLS firstYou sent AUTH or MAIL on a plaintext connection to 587.
535 Invalid API keyThe key is wrong, revoked or expired.
535 Invalid email or passwordThe password was not an API key, and no active mailbox matched the address and password.
550 From address must be …Mailbox credentials can only send as that mailbox’s own address.
550 Domain … not found for this teamThe From domain is not on your team, or … is not verified.
550 All recipients are suppressedEvery recipient — or every To recipient — is on your suppression list. Otherwise suppressed recipients are dropped and the rest is sent.
550 Verify your email address before sending email.The team owner has not confirmed their email address yet.
451 Monthly email limit reached. Upgrade your plan.Your plan’s quota for this month is used up.
450 …A fault on our side, a busy server or the mailbox rate limit. Retry later.

Mailbox credentials#

When the password is not an API key, the relay treats the username as a mailbox address and checks the mailbox password. This is what a desktop or phone mail client uses. Compared with an API key:

  • The username must be the full address, e.g. anna@yourdomain.com, and the mailbox must be active.
  • The From address must be exactly that mailbox; anything else gets 550 From address must be anna@yourdomain.com.
  • A copy of each sent message is saved to the mailbox's Sent folder.
  • Sending is limited to 60 messages per hour per mailbox.
  • Sends are always live — there is no test mode for mailbox credentials.

Test the connection#

To separate a network or credentials problem from a problem in your application, send one message from a terminal with swaks:

bash
swaks --server smtp.poststack.dev --port 587 --tls \
  --auth PLAIN --auth-user poststack --auth-password "$POSTSTACK_API_KEY" \
  --from hello@yourdomain.com --to you@example.com \
  --header "Subject: PostStack SMTP test"

Or inspect the TLS handshake and capabilities without sending:

bash
# See the certificate and the server's capabilities without sending anything.
openssl s_client -connect smtp.poststack.dev:587 -starttls smtp -quiet
# then type:  EHLO test.example   →   250-AUTH PLAIN LOGIN … 250 SIZE 37748736
#             QUIT

Troubleshooting#

The connection times out, or hangs right after connecting

Many hosting providers and cloud platforms block outbound SMTP ports. Check that your server can reach smtp.poststack.dev on 587 or 465 at all (the openssl command above). On a plaintext connection to 587 the greeting arrives about 1.5 seconds after connecting — the relay waits that long to see whether your client starts TLS — so a client-side greeting timeout shorter than that will fail.

“wrong version number” or a TLS handshake error

Your client and port disagree about how TLS starts. With nodemailer, secure: true means implicit TLS: use it with 465, and secure: false with 587. In Python use SMTP_SSL on 465 and SMTP + starttls() on 587.

530 5.7.0 Must issue STARTTLS first

Your client connected to 587 and tried to authenticate without upgrading. Turn on STARTTLS (requireTLS: true, EMAIL_USE_TLS = True, enable_starttls_auto: true) or switch to 465 with implicit TLS.

421 You talk too soon

A hand-written client sent EHLO before reading the server's 220 greeting. Wait for the greeting first; every SMTP library already does.

The send succeeds but nothing arrives

Check the reply text: if it says TEST MODE, you are using an sk_test_… key. Otherwise open the email under Emails in the dashboard — its events show whether it was delivered, bounced or suppressed.

The recipient sees a PostStack Message-ID, or my custom header is missing

Expected: only X- headers and the threading headers are carried through, and the outgoing message gets a new Message-ID. Rename a custom header to start with X- to have it delivered.

Next steps

Was this page helpful?

Related

SMTP relay overviewUsing PostStack from WordPress, Laravel, Rails and any SMTP client.

Framework guides