SMTP relay
Point any mail library at smtp.koltrix.com:2525 and authenticate with your
API key. The relay parses the submitted MIME, enqueues a TaskSendEmail
through the existing worker pipeline, and returns 250 2.0.0 OK: queued.
Identical pipeline to the REST API — same bounce handling, suppression,
tracking, warmup, and webhooks.
When in doubt: REST is more ergonomic, SMTP is more compatible. Use REST for new code, SMTP when you have something that already speaks SMTP.
Connection details
| Setting | Value |
|---|---|
| Host | smtp.koltrix.com |
| Port | 2525 |
| TLS | TLS terminates at the cluster edge — STARTTLS on :2525 returns 454. The hop between the edge and the relay is on a private network. |
| Mechanism | AUTH PLAIN or AUTH LOGIN |
| Username | apikey (the relay accepts any non-empty value) |
| Password | Your full kx_… key — must include the send permission |
| Max size | 25 MB per message |
The relay advertises 8BITMIME, SIZE 26214400, AUTH PLAIN LOGIN, and
HELP in response to EHLO. Standard SMTP commands work — MAIL FROM,
RCPT TO, DATA, RSET, NOOP, QUIT.
Why kx_ keys instead of mailbox passwords?
A leaked SMTP password is a leaked sender. By using your API key as the password, the relay can:
- Resolve the right tenant (the key is scoped to one organisation).
- Check the
sendpermission scope before accepting the message. - Apply the same suppression and warmup checks the REST handler does.
- Get revoked instantly from Settings → API Keys without touching mail config.
Examples
swaks (one-liner)
The fastest way to verify your key works:
swaks --server smtp.koltrix.com --port 2525 \
--auth PLAIN --auth-user apikey --auth-password "$KOLTRIX_KEY" \
--from [email protected] --to [email protected] \
--header "Subject: Hello via SMTP" \
--body "Plain body"Expected response after DATA:
250 2.0.0 OK: queuedPython (stdlib)
import smtplib, email.message, os
msg = email.message.EmailMessage()
msg["From"] = "[email protected]"
msg["To"] = "[email protected]"
msg["Subject"] = "Hello via SMTP"
msg.set_content("Plain body")
msg.add_alternative("<p>HTML body</p>", subtype="html")
with smtplib.SMTP("smtp.koltrix.com", 2525) as s:
s.login("apikey", os.environ["KOLTRIX_KEY"])
s.send_message(msg)Node (nodemailer)
import nodemailer from "nodemailer";
const t = nodemailer.createTransport({
host: "smtp.koltrix.com",
port: 2525,
secure: false, // STARTTLS not used on :2525
auth: { user: "apikey", pass: process.env.KOLTRIX_KEY! },
});
await t.sendMail({
from: "[email protected]",
to: "[email protected]",
subject: "Hello via SMTP",
text: "Plain body",
html: "<p>HTML body</p>",
attachments: [
{ filename: "invoice.pdf", path: "./invoice.pdf" },
],
});Go (net/smtp)
package main
import (
"net/smtp"
"os"
)
func main() {
auth := smtp.PlainAuth("", "apikey", os.Getenv("KOLTRIX_KEY"), "smtp.koltrix.com")
msg := []byte("To: [email protected]\r\n" +
"From: [email protected]\r\n" +
"Subject: Hello via SMTP\r\n" +
"MIME-Version: 1.0\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n" +
"\r\n" +
"<p>HTML body</p>\r\n")
if err := smtp.SendMail("smtp.koltrix.com:2525", auth, "[email protected]",
[]string{"[email protected]"}, msg); err != nil {
panic(err)
}
}PHP (PHPMailer)
$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host = "smtp.koltrix.com";
$mail->Port = 2525;
$mail->SMTPAuth = true;
$mail->Username = "apikey";
$mail->Password = getenv("KOLTRIX_KEY");
$mail->setFrom("[email protected]");
$mail->addAddress("[email protected]");
$mail->Subject = "Hello via SMTP";
$mail->msgHTML("<p>Hi</p>");
$mail->send();Ruby
require "mail"
Mail.defaults do
delivery_method :smtp,
address: "smtp.koltrix.com",
port: 2525,
user_name: "apikey",
password: ENV["KOLTRIX_KEY"],
authentication: :plain
end
Mail.deliver do
from "[email protected]"
to "[email protected]"
subject "Hello via SMTP"
body "Plain body"
endWire-level walkthrough
If you're debugging by hand, the conversation looks like this:
S: 220 koltrix-smtp ready
C: EHLO acme.com
S: 250-koltrix-smtp
S: 250-AUTH PLAIN LOGIN
S: 250-8BITMIME
S: 250-SIZE 26214400
S: 250 HELP
C: AUTH PLAIN AGFwaWtleQBreF9saXZlXy4uLg==
S: 235 2.7.0 Authentication successful
C: MAIL FROM:<[email protected]> SIZE=571
S: 250 2.1.0 OK
C: RCPT TO:<[email protected]>
S: 250 2.1.5 OK
C: DATA
S: 354 End data with <CR><LF>.<CR><LF>
C: From: [email protected]
C: To: [email protected]
C: Subject: Hello via SMTP
C:
C: Plain body
C: .
S: 250 2.0.0 OK: queued
C: QUIT
S: 221 2.0.0 ByeThe base64 string in AUTH PLAIN is \0apikey\0kx_... — null byte,
username, null byte, password.
Error codes
| Code | Meaning |
|---|---|
220 | Server ready |
235 | Auth successful |
250 | Command accepted / message queued |
354 | Begin sending message body |
454 | STARTTLS not available on this port |
501 | Bad address syntax (often missing <> around the address) |
503 | Out-of-order command (RCPT before MAIL FROM, DATA before RCPT) |
530 | Authentication required |
535 | Authentication failed — bad key, revoked key, or missing send scope |
554 | Message rejected — typically a missing/invalid From |
Tracking and webhooks still apply
Every message that comes in through SMTP gets the same treatment as a REST send:
- Suppression list checked per recipient.
- Warmup quota debited.
- DKIM signed automatically by the platform.
- Open pixel + tracked links injected (unless the relay is told not to —
pass
X-Koltrix-No-Track: 1in the headers to opt out). - Webhooks fire for
message.sent,message.delivered,message.bounced, etc.
The send shows up under Settings → Email logs alongside REST sends — they're not distinguished in the UI because they're not distinguished in the pipeline.
When to use SMTP vs REST
| Use REST when… | Use SMTP when… |
|---|---|
| You're writing new code in a typed language | You have legacy code that already speaks SMTP |
You need Idempotency-Key semantics | A third-party tool only supports SMTP credentials |
| You want structured JSON error responses | You need to keep using a vendored mail library (PHPMailer, etc.) |
| You're sending to many recipients with per-recipient metadata | You're shipping a customer-facing "use your own SMTP" feature |
If you find yourself building a wrapper around the SMTP relay just to add JSON, switch to REST — it's exactly the same pipeline with a much nicer client surface.
Common pitfalls
- Wrong port. The customer-facing relay is on
2525, not25or587. Other ports are internal and aren't accepting customer traffic. - Trying STARTTLS on :2525. The relay returns
454for STARTTLS intentionally. TLS terminates at the cluster edge — the inner hop is on a private network. Your client's connection tosmtp.koltrix.com:2525is already TLS-wrapped at the edge. - No
Fromheader in the DATA payload. The relay accepts the envelopeMAIL FROMbut receivers will reject messages that have no matchingFrom:header in the body. Most libraries set both automatically; if you're writing raw SMTP, set theFrom:header yourself. - Sending without the
sendpermission. The key authenticates fine but everyRCPT TOwill return535. Mint a new key with thesendscope in Settings → API Keys. - Hitting the size cap. 25 MB is the hard ceiling. The relay rejects
larger messages mid-DATA with
552 5.3.4 Message too largeand the whole transaction has to be retried with a smaller payload.
Monitoring the relay
Every accepted message shows up under Settings → Email logs in the
dashboard alongside REST sends — they're not distinguished in the UI
because they share the same outbound table and pipeline. Each row
includes the receiving SMTP server's reply, so a bounced row tells
you exactly why.
Failed authentications surface as 535 responses to your client.
Common causes: the key was revoked, the key was rotated and you're
still using the old one, or the key lacks the send scope. Mint a
new key with the send scope at
app.koltrix.com/api-keys and your
client should auth immediately.