KoltrixKoltrix docs

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

SettingValue
Hostsmtp.koltrix.com
Port2525
TLSTLS terminates at the cluster edge — STARTTLS on :2525 returns 454. The hop between the edge and the relay is on a private network.
MechanismAUTH PLAIN or AUTH LOGIN
Usernameapikey (the relay accepts any non-empty value)
PasswordYour full kx_… key — must include the send permission
Max size25 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 send permission 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: queued

Python (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"
end

Wire-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 Bye

The base64 string in AUTH PLAIN is \0apikey\0kx_... — null byte, username, null byte, password.

Error codes

CodeMeaning
220Server ready
235Auth successful
250Command accepted / message queued
354Begin sending message body
454STARTTLS not available on this port
501Bad address syntax (often missing <> around the address)
503Out-of-order command (RCPT before MAIL FROM, DATA before RCPT)
530Authentication required
535Authentication failed — bad key, revoked key, or missing send scope
554Message 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: 1 in 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 languageYou have legacy code that already speaks SMTP
You need Idempotency-Key semanticsA third-party tool only supports SMTP credentials
You want structured JSON error responsesYou need to keep using a vendored mail library (PHPMailer, etc.)
You're sending to many recipients with per-recipient metadataYou'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, not 25 or 587. Other ports are internal and aren't accepting customer traffic.
  • Trying STARTTLS on :2525. The relay returns 454 for STARTTLS intentionally. TLS terminates at the cluster edge — the inner hop is on a private network. Your client's connection to smtp.koltrix.com:2525 is already TLS-wrapped at the edge.
  • No From header in the DATA payload. The relay accepts the envelope MAIL FROM but receivers will reject messages that have no matching From: header in the body. Most libraries set both automatically; if you're writing raw SMTP, set the From: header yourself.
  • Sending without the send permission. The key authenticates fine but every RCPT TO will return 535. Mint a new key with the send scope 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 large and 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.