KoltrixKoltrix docs

Sending email

There are three ways to send mail through Koltrix and they all share the same pipeline — the same outbound table, the same suppression list, the same DKIM signer, the same webhook stream:

  1. REST APIPOST /api/v2/emails. The standard integration for backend code.
  2. SMTP relay — port 2525, AUTH PLAIN with your API key as the password. The drop-in for libraries that already speak SMTP.
  3. Broadcasts — send to a newsletter list via POST /api/v2/lists/:id/broadcast. Covered in Newsletters.

This page covers (1) in depth.

POST /api/v2/emails

curl https://api.koltrix.com/api/v2/emails \
  -H "Authorization: Bearer kx_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "from": "[email protected]",
    "to":  ["[email protected]"],
    "cc":  ["[email protected]"],
    "bcc": ["[email protected]"],
    "subject": "Your invoice for May",
    "body_html": "<h1>Thanks!</h1><p>See you next month.</p>",
    "body_text": "Thanks!\n\nSee you next month."
  }'

Required scope

The API key must include the send scope. Without it the call returns 403 missing permission: send.

Request body

FieldTypeNotes
fromstringRequired. Must be an active address on a verified sending domain for the account.
tostring[]Required. At least one recipient.
ccstring[]Optional.
bccstring[]Optional. Stripped from the visible headers.
subjectstringRequired.
body_htmlstringHTML body. Provide either this, body_text, or both.
body_textstringPlain-text body. Provide either this, body_html, or both.

The from address is enforced strictly: it must match an exact mailbox that's been added under Settings → Domains → <domain> → Add address and marked active. Unknown local-parts on a verified domain are rejected — the API tells you which case failed:

{
  "error": "from address '[email protected]' is not registered for this account. The domain is verified — add this exact address in Dashboard → Domains."
}

Headers

HeaderNotes
AuthorizationRequired. Bearer kx_... with the send scope.
Content-Typeapplication/json
Idempotency-KeyStrongly recommended. Any unique string per logical send. Cached 24h.

Response

{
  "id": "01HX9C8...",
  "status": "queued",
  "queued": true,
  "tracking_url": "/api/v2/messages/01HX9C8...",
  "recipient_count": 1
}

The request returns 202 Accepted once the job is on the queue. Actual SMTP delivery happens inside the worker — usually within a second. Poll GET /api/v2/messages/{id} for the current status and counters, or subscribe to webhooks and stop polling entirely.

Status flow

Each send walks through these states:

queued → sent → opened → clicked → replied
       ↘ bounced  (5xx — recipient auto-suppressed)
       ↘ failed   (unrecoverable error before SMTP)

queued → sent happens inside the worker once delivery is accepted by the receiving server. Opens and clicks are recorded by the tracking pixel and link rewriter respectively; replies are matched on inbound mail.

Idempotency

When the network blinks and your client retries, you don't want two copies of the welcome email. Include Idempotency-Key: <unique-string> and we cache the first response for 24 hours per (account, key). Subsequent calls with the same key return the original response with header Idempotent-Replayed: true — no second send happens.

Generate the key once per logical send (a UUIDv4 from your DB, or a hash of the message contents) and reuse it across retries. Don't generate a new key inside the retry loop — that defeats the purpose.

Tracking and how to opt out

Open and click tracking are on by default. The worker:

  • Injects a 1×1 tracking pixel just before </body>. A hit on the pixel marks the message as opened and fires message.opened.
  • Rewrites every tracked <a href="https://..."> through a redirect endpoint. Clicks land at the original URL after recording the event and firing message.clicked.

To opt out of tracking on a specific transactional send (good for password resets and receipts), use the SMTP relay with header X-Koltrix-No-Track: 1. To opt out at the campaign level for broadcasts, untick "Track opens" / "Track clicks" in the composer.

The List-Unsubscribe header is added automatically to broadcast sends regardless of tracking flags — that's a deliverability requirement, not analytics.

Polling a sent message

curl https://api.koltrix.com/api/v2/messages/01HX9C8... \
  -H "Authorization: Bearer kx_..."

Returns the canonical row:

{
  "id": "01HX9C8...",
  "from": "[email protected]",
  "to": "[email protected]",
  "subject": "Your invoice for May",
  "status": "opened",
  "open_count": 2,
  "click_count": 1,
  "reply_count": 0,
  "engagement_score": 65,
  "first_open_after_seconds": 423,
  "message_id": "<[email protected]>",
  "created_at": "2026-05-22T14:00:00Z",
  "sent_at":    "2026-05-22T14:00:01Z",
  "opened_at":  "2026-05-22T14:07:04Z",
  "clicked_at": "2026-05-22T14:09:12Z"
}

For the per-event timeline (every open + click + reply with IP, UA, prefetch flag) hit GET /api/v2/messages/{id}/events.

Code samples

Node (fetch)

import { randomUUID } from "node:crypto";
 
await fetch("https://api.koltrix.com/api/v2/emails", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.KOLTRIX_KEY!}`,
    "Content-Type": "application/json",
    "Idempotency-Key": randomUUID(),
  },
  body: JSON.stringify({
    from: "[email protected]",
    to: ["[email protected]"],
    subject: "Hello",
    body_html: "<p>Hi</p>",
    body_text: "Hi",
  }),
});

Python (requests)

import os, uuid, requests
 
requests.post(
    "https://api.koltrix.com/api/v2/emails",
    headers={
        "Authorization": f"Bearer {os.environ['KOLTRIX_KEY']}",
        "Content-Type": "application/json",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "from": "[email protected]",
        "to": ["[email protected]"],
        "subject": "Hello",
        "body_html": "<p>Hi</p>",
        "body_text": "Hi",
    },
)

Go (net/http)

body := strings.NewReader(`{
  "from": "[email protected]",
  "to":   ["[email protected]"],
  "subject": "Hello",
  "body_html": "<p>Hi</p>",
  "body_text": "Hi"
}`)
req, _ := http.NewRequest("POST", "https://api.koltrix.com/api/v2/emails", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", uuid.NewString())
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()

SMTP alternative

If you have a library that already speaks SMTP, point it at smtp.koltrix.com:2525 with AUTH PLAIN:

SettingValue
Hostsmtp.koltrix.com
Port2525
Usernameapikey (any value works)
PasswordYour full kx_ key (with send)

Full client examples in SMTP relay.