# Koltrix documentation > Koltrix is email for SaaS teams on your own domain: a transactional email REST API (POST /api/v2/emails), an SMTP relay, signed webhooks, a team inbox for replies, and an MCP server so Claude, ChatGPT and Cursor can read, organise and draft mail. This file is every page of https://docs.koltrix.com, in sidebar order, as Markdown, followed by the API reference for every endpoint. Index: https://docs.koltrix.com/llms.txt. Any single page: add `.md` to its URL. --- # Koltrix developer docs Source: https://docs.koltrix.com **Email for your product, sent from your own domain.** Send transactional email through a REST API or SMTP, get delivery events by webhook, and read the replies in a shared team inbox. **Build with AI.** Copy one prompt into Claude Code, Cursor or ChatGPT. It reads these docs, asks you a few questions and writes the integration. See [Build with AI](https://docs.koltrix.com/ai.md). ## Start here 1. **Add your domain** Five DNS records, about ten minutes of work. [Domains and DNS](https://docs.koltrix.com/domains.md) 2. **Create an API key** Under **Settings → API keys**, shown once. [Authentication](https://docs.koltrix.com/authentication.md) 3. **Send an email** One `POST` to `/api/v2/emails`. [Sending email](https://docs.koltrix.com/sending-email.md) The [Quickstart](https://docs.koltrix.com/quickstart.md) walks through all three, with the commands to copy. ## Explore - [Sending email](https://docs.koltrix.com/sending-email.md): The send API in depth: retries, statuses, errors. - [SMTP relay](https://docs.koltrix.com/smtp-relay.md): Point any SMTP library or app at Koltrix. - [Webhooks](https://docs.koltrix.com/webhooks.md): Sent, bounced, opened and clicked events, signed. - [API reference](https://docs.koltrix.com/api-reference.md): Every endpoint, with a live Try it. - [Replies](https://docs.koltrix.com/replies.md): Answers to your mail land in the team inbox. - [Connect an assistant](https://docs.koltrix.com/mcp.md): Let Claude or ChatGPT read, sort and draft mail. ## What Koltrix doesn't do - **No SMS or push notifications.** Email only. - **Koltrix's own AI never sends, forwards or deletes mail for you.** Its triage, summaries and drafted replies only suggest; every message is sent by a person clicking Send in Koltrix. An assistant you connect over the [AI assistants connection](https://docs.koltrix.com/mcp.md) can read, sort and draft, and can send only if an admin turns that on, you opt in, and you confirm each message in the assistant. It can never forward or delete. - **No sending through other providers.** Koltrix delivers from its own mail servers. - **No compliance certifications yet.** Koltrix is not SOC 2 or HIPAA certified. If you need that, email us before you sign up. ## Help Email **support@koltrix.com**; we answer within one business day. Common problems and their fixes are in [Troubleshooting](https://docs.koltrix.com/troubleshooting.md), and what changed recently is in the [Changelog](https://docs.koltrix.com/changelog.md). --- # Quickstart Source: https://docs.koltrix.com/quickstart This takes you from a new account to an email delivered through the API. Allow about fifteen minutes; most of it is waiting for DNS. 1. Create a workspace. 2. Add your domain and an address on it. 3. Create an API key. 4. Send an email. 1. **Create a workspace** Sign up at [app.koltrix.com](https://app.koltrix.com) and name your workspace. You are its owner; invite teammates later from **Settings → Team**. New workspaces start on a trial with tighter limits. They are listed under [Limits and quotas](https://docs.koltrix.com/limits.md#during-the-trial). 2. **Add your domain and an address** Open **Settings → Domains & addresses** and click **Add domain**. Enter the domain you want to send from, such as `acme.com`. Koltrix shows five DNS records to add at your DNS provider, each with a copy button: | Record | Name | What it does | | --- | --- | --- | | Ownership (TXT) | `_koltrix` | Proves you control the domain. | | MX | `@` | Delivers your domain's mail, including replies, to Koltrix. | | SPF (TXT) | `@` | Lets Koltrix send for the domain. If you already have an SPF record, Koltrix shows a merged one to replace it with. | | DKIM (TXT) | `kx1._domainkey` | Signs your mail so it can't be forged. | | DMARC (TXT) | `_dmarc` | Tells receivers what to do with forgeries. | [Custom domains and DNS](https://docs.koltrix.com/domains.md) explains each record, with notes for Cloudflare, GoDaddy, Namecheap and Route 53. Click **Check again** after you have saved them. DNS usually takes a few minutes and sometimes a few hours. The domain becomes **verified** when all five records check out. Then click **Add address** on the domain and add the mailbox you will send from, such as `hello@acme.com`. The API only sends from addresses added this way; any other From address is refused, even on a verified domain. 3. **Create an API key** Open **Settings → API keys** and click **Create key**. Name it (for example "Local development") and keep the default scopes, `read` and `send`. The key starts with `kx_` and is **shown once**. Copy it now: ```bash export KOLTRIX_API_KEY="kx_..." ``` Check it works: ```bash curl https://api.koltrix.com/api/v2/me -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` 4. **Send an email** ```bash curl https://api.koltrix.com/api/v2/emails \ -H "Authorization: Bearer $KOLTRIX_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: quickstart-1" \ -d '{ "from": "hello@acme.com", "to": ["you@your-personal-inbox.com"], "subject": "Hello from Koltrix", "body_html": "
Sent with the Koltrix API.
" }' ``` Koltrix answers `202 Accepted`: ```json { "id": "6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20", "status": "queued", "queued": true, "tracking_url": "/api/v2/messages/6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20", "recipient_count": 1 } ``` The email arrives a few seconds later. A copy is in your Koltrix Sent folder, and reply to it from your personal inbox to see the reply arrive in Koltrix. Check its status: ```bash curl https://api.koltrix.com/api/v2/messages/6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20 \ -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` `status` moves from `queued` to `sent`, and `open_count` goes up when you open it. (Right after sending it may still show `queued`; check again in a second.) The same request in Node, Python, Go and PHP is in [Sending email](https://docs.koltrix.com/sending-email.md#code-samples). ## If it didn't arrive | What you see | What to do | | --- | --- | | `403` "from address … is not registered" | Add that exact address to the domain (step 2). The message says whether the domain itself is verified yet. | | `401` | The key is wrong or revoked. Check you copied all of it, including `kx_`. | | `status` is `bounced` | The `error` field has the receiving server's reason. A typo in the address is the usual cause. | | `status` is `sent` but nothing arrived | Look in your spam folder, then check the domain's records in Settings: an unverified DKIM record is the usual cause. | More in [Troubleshooting](https://docs.koltrix.com/troubleshooting.md). ## Next - [Webhooks](https://docs.koltrix.com/webhooks.md): hear about sends, bounces and opens without polling. - [SMTP relay](https://docs.koltrix.com/smtp-relay.md): for software that only speaks SMTP. - [Receiving replies](https://docs.koltrix.com/replies.md): what happens when customers write back. - [AI assistants](https://docs.koltrix.com/mcp.md): connect Claude, ChatGPT or Cursor to your inbox. --- # Authentication Source: https://docs.koltrix.com/authentication Your code authenticates to Koltrix with an **API key**. The same key works for the REST API (`https://api.koltrix.com/api/v2`) and, with the `send` scope, as the password for the [SMTP relay](https://docs.koltrix.com/smtp-relay.md). | Surface | How it authenticates | | --- | --- | | REST API, `/api/v2/*` | `Authorization: Bearer kx_…` | | SMTP relay, port 2525 | `AUTH PLAIN` or `AUTH LOGIN`, the API key as the password | | AI assistants (MCP) | OAuth sign-in with your Koltrix account, never an API key. See [AI assistants](https://docs.koltrix.com/mcp.md). | | The Koltrix app | Your account sign-in, in the browser | An API key belongs to a **workspace**, not to a person. Anyone holding it can act as that workspace within the key's scopes, so keep keys on your servers and never put one in a browser or a mobile app. ## Create a key Owners and admins create keys under **Settings → API keys** (also linked from **Developers**). Give the key a name you'll recognise later, such as "Production web" or "Billing worker", and choose its scopes. - The key looks like `kx_` followed by 48 hexadecimal characters. - **It is shown exactly once.** Copy it into your secret store straight away. Koltrix stores only a hash and cannot show it again. - The list shows each key's first characters, its scopes and when it was last used, so you can tell keys apart and spot unused ones. ```bash export KOLTRIX_API_KEY="kx_..." ``` ## Scopes A key may carry several scopes. Give each key the fewest it needs. New keys start with `read` and `send`. | Scope | What it allows | | --- | --- | | `send` | Send email with `POST /api/v2/emails` and through the SMTP relay. Also reads back your messages. | | `read` | Read your message log and message events, and read lists, subscribers and sequences. | | `newsletter` | Create, change and delete lists, subscribers, broadcasts and sequences. | | `contacts` | Reserved. No `/api/v2` endpoint uses it today. | | `*` | Full access: every scope, now and in future. Use it sparingly. | Scopes are fixed when the key is created. To change them, create a new key and revoke the old one. ### Which scope each endpoint needs | Endpoint | Scope | | --- | --- | | `GET /api/v2/me` | Any valid key | | `POST /api/v2/emails` | `send` | | `GET /api/v2/messages`, `GET /api/v2/messages/:id`, `GET /api/v2/messages/:id/events` | `read` or `send` | | `GET` on lists, subscribers, sequences and sequence steps | `read` or `newsletter` | | Every other list, subscriber, broadcast and sequence endpoint | `newsletter` | | SMTP relay | `send` | ## Make an authenticated request `GET /api/v2/me` tells you which workspace a key belongs to and what it may do. It is a good first call and a good health check. ```bash curl https://api.koltrix.com/api/v2/me \ -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` ```json { "tenant": "acme", "permissions": ["read", "send"] } ``` `tenant` is your workspace's short name. ## Errors | Status | Body | What to do | | --- | --- | --- | | `401` | `{"error": "missing Bearer token"}` | Send the header as `Authorization: Bearer kx_…`, with the word `Bearer` and a space. | | `401` | `{"error": "invalid api key format"}` | The token doesn't start with `kx_`. Check you copied the whole key. | | `401` | `{"error": "invalid or revoked api key"}` | The key doesn't exist or was revoked. Create a new one. | | `403` | `{"error": "missing permission: send"}` | The key is valid but lacks the scope. The message names it (`send`, `read` or `newsletter`). | Endpoints that accept `read` **or** `newsletter` name only `newsletter` in their `403` message, and endpoints that accept `read` or `send` name `read`. ## Rotate a key Keys don't expire. To rotate one: 1. Create a new key with the same scopes. 2. Deploy it everywhere the old key is used, including SMTP settings. 3. Revoke the old key under **Settings → API keys**. Revoking takes effect on the next request: the old key gets `401` on the API and `535` on the SMTP relay. A request that was already accepted finishes normally. Rotate straight away if a key is ever committed to a repository, pasted in a chat or printed in a log, and otherwise on a schedule that suits you. ## Rate limits Each API key may make **60 requests per minute** to `/api/v2`. Over that, the API answers `429` with a `Retry-After` header. This limit is separate from your plan's send quota; both are explained in [Limits and quotas](https://docs.koltrix.com/limits.md#rate-limits). ## Calling the API from a browser The API answers cross-origin requests from any website (without cookies), so a browser can call it. Don't do that from your own pages: a key in a web page is a key anyone can copy. Call the API from your server. The **Try it** panels in the [API reference](https://docs.koltrix.com/api-reference.md) are the one exception. They run in your browser on docs.koltrix.com and send the key you paste straight to `api.koltrix.com`, nowhere else, and keep it only in that browser tab. Use a dedicated key with the fewest permissions and revoke it afterwards. --- # Sending email Source: https://docs.koltrix.com/sending-email Koltrix sends mail for your application in two ways, through the same pipeline (one suppression list, one signing setup, one message log, the same webhooks): 1. **The REST API**: `POST /api/v2/emails`. The right choice for new code. 2. **The [SMTP relay](https://docs.koltrix.com/smtp-relay.md)**: for software that already speaks SMTP. This page covers the REST API. ## Before you send - **An API key with the `send` scope.** See [Authentication](https://docs.koltrix.com/authentication.md). - **A registered From address.** The `from` address must be an active address on a domain that is verified in your workspace. Add the domain under **Settings → Domains & addresses**, publish its [DNS records](https://docs.koltrix.com/domains.md), then click **Add address** on the domain and add the exact mailbox you will send from, for example `hello@acme.com`. ## `POST /api/v2/emails` **POST** `https://api.koltrix.com/api/v2/emails` **cURL** ```bash curl https://api.koltrix.com/api/v2/emails \ -H "Authorization: Bearer $KOLTRIX_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-1042-receipt" \ -d '{ "from": "AcmeYour order is on its way.
", "body_text": "Thanks! Your order is on its way." }' ``` ### Request body | Name | Type | Required | Description | | --- | --- | --- | --- | | `from` | `string` | Yes | `hello@acme.com` or `AcmeThanks for your order.
" }' ``` **Node.js** ```ts // Node 18+: fetch is built in. const res = await fetch("https://api.koltrix.com/api/v2/emails", { method: "POST", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": "order-1042-receipt", }, body: JSON.stringify({ from: "hello@acme.com", to: ["lead@example.com"], subject: "Your receipt", body_html: "Thanks for your order.
", }), }); if (!res.ok) throw new Error(`Koltrix ${res.status}: ${await res.text()}`); const { id } = await res.json(); ``` **Python** ```python res = requests.post( "https://api.koltrix.com/api/v2/emails", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", "Idempotency-Key": "order-1042-receipt", }, json={ "from": "hello@acme.com", "to": ["lead@example.com"], "subject": "Your receipt", "body_html": "Thanks for your order.
", }, timeout=10, ) res.raise_for_status() message_id = res.json()["id"] ``` **Go** ```go package main "bytes" "encoding/json" "fmt" "net/http" "os" ) func main() { body, _ := json.Marshal(map[string]any{ "from": "hello@acme.com", "to": []string{"lead@example.com"}, "subject": "Your receipt", "body_html": "Thanks for your order.
", }) req, _ := http.NewRequest("POST", "https://api.koltrix.com/api/v2/emails", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) req.Header.Set("Content-Type", "application/json") req.Header.Set("Idempotency-Key", "order-1042-receipt") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) // safe to retry with the same Idempotency-Key } defer resp.Body.Close() var out struct { ID string `json:"id"` } _ = json.NewDecoder(resp.Body).Decode(&out) fmt.Println(resp.StatusCode, out.ID) } ``` **PHP** ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), "Content-Type: application/json", "Idempotency-Key: order-1042-receipt", ], CURLOPT_POSTFIELDS => json_encode([ "from" => "hello@acme.com", "to" => ["lead@example.com"], "subject" => "Your receipt", "body_html" => "Thanks for your order.
", ]), ]); $response = json_decode(curl_exec($ch), true); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); ``` There are no official SDKs yet. The API is small enough that a few lines like these are all you need. ## REST or SMTP? | Use the REST API when… | Use the SMTP relay when… | | --- | --- | | You are writing new code | Your software can only send by SMTP | | You want `Idempotency-Key` retries | You can't change the code, only its settings | | You want the message id back straight away | A third-party tool asks for SMTP credentials | | You want JSON errors that say what to fix | | The relay has its own rules and limits; read [SMTP relay](https://docs.koltrix.com/smtp-relay.md) before you use it. --- # SMTP relay Source: https://docs.koltrix.com/smtp-relay Point software that already speaks SMTP at Koltrix and authenticate with an API key. Messages accepted by the relay go through the same pipeline as the [REST API](https://docs.koltrix.com/sending-email.md): the same suppression list, signing, message log, send quota and webhooks. If you are writing new code, use the REST API instead. It returns the message id, supports idempotent retries and tells you in JSON what went wrong. ## Connection settings | Setting | Value | | --- | --- | | Host | `smtp.koltrix.com` | | Port | `2525` | | Encryption | `STARTTLS` (TLS 1.2 or later, certificate for `smtp.koltrix.com`). Connect, send `EHLO`, upgrade with `STARTTLS`, then authenticate. Most libraries do this automatically. | | Authentication | `AUTH PLAIN` or `AUTH LOGIN` | | Username | `apikey` (any value is accepted) | | Password | An API key with the `send` scope | | Message size | 25 MB per message | | Recipients | 1,000 per message | | Connection time | A connection is closed after 5 minutes | Ports `587` and `465` are **not** the relay and don't accept API keys. > **Authenticate after STARTTLS** > > The relay still accepts `AUTH` on an unencrypted connection so existing integrations keep working, but that will be switched off. Always upgrade with `STARTTLS` before you log in: your API key is a password. ## What the relay reads from your message The relay is built for transactional mail with a simple body. It takes: | From your SMTP session | Used as | | --- | --- | | `MAIL FROM` (the envelope sender) | The **From address** of the sent message. It must be an active address on a verified domain in **your** workspace, exactly as for the [REST API](https://docs.koltrix.com/sending-email.md#before-you-send); anything else is refused with `553 5.7.1 Sender not authorized`. | | Each `RCPT TO` | A recipient. Recipients named in your message's `To:` or `Cc:` header appear there; any other `RCPT TO` is delivered as a Bcc and never shown in the headers. | | The `Subject:` header | The subject. | | The body of a single-part `text/plain` or `text/html` message | The message body. An HTML body also gets a plain-text version generated from it. | The `To:` and `Cc:` headers only decide how each `RCPT TO` recipient is shown. Everything else in the message is ignored: the `From:`, `Bcc:` and `Reply-To:` headers, display names and any custom headers. Put every recipient in `RCPT TO`. > **Current limitation** > > The relay does not parse multipart messages, so a > message with both a text and an HTML part, or with attachments, arrives > garbled. Send a single-part message: most libraries do that when you give > them only `text` or only `html` and no attachments. > **Current limitation** > > Mail sent through the relay has no open or click > tracking, and the relay doesn't return a message id. Find the message in > **Settings → Logs** or with `GET /api/v2/messages?recipient=…`. ## Examples ### swaks The quickest way to check that a key works: ```bash swaks --server smtp.koltrix.com --port 2525 --tls \ --auth PLAIN --auth-user apikey --auth-password "$KOLTRIX_API_KEY" \ --from hello@acme.com --to lead@example.com \ --header "Subject: Hello via SMTP" \ --body "It works." ``` The last reply should be `250 2.0.0 OK: queued`. ### Node.js (nodemailer) **Node.js** ```ts const transport = nodemailer.createTransport({ host: "smtp.koltrix.com", port: 2525, secure: false, auth: { user: "apikey", pass: process.env.KOLTRIX_API_KEY }, }); await transport.sendMail({ from: "hello@acme.com", to: "lead@example.com", subject: "Hello via SMTP", html: "It works.
", // one body only: html or text, not both }); ``` ### Python (smtplib) **Python** ```python from email.message import EmailMessage msg = EmailMessage() msg["From"] = "hello@acme.com" msg["To"] = "lead@example.com" msg["Subject"] = "Hello via SMTP" msg.set_content("It works.
", subtype="html") # a single HTML part with smtplib.SMTP("smtp.koltrix.com", 2525) as s: s.starttls() s.login("apikey", os.environ["KOLTRIX_API_KEY"]) s.send_message(msg) ``` ### Go (net/smtp) `smtp.SendMail` upgrades to STARTTLS on its own, and `smtp.PlainAuth` then sends the key inside the encrypted connection: **Go** ```go package main "net/smtp" "os" ) func main() { msg := []byte("From: hello@acme.com\r\n" + "To: lead@example.com\r\n" + "Subject: Hello via SMTP\r\n" + "Content-Type: text/html; charset=UTF-8\r\n" + "\r\n" + "It works.
\r\n") auth := smtp.PlainAuth("", "apikey", os.Getenv("KOLTRIX_API_KEY"), "smtp.koltrix.com") err := smtp.SendMail("smtp.koltrix.com:2525", auth, "hello@acme.com", []string{"lead@example.com"}, msg) if err != nil { panic(err) } } ``` ### PHP (PHPMailer) **PHP** ```php isSMTP(); $mail->Host = "smtp.koltrix.com"; $mail->Port = 2525; $mail->SMTPAuth = true; $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; $mail->Username = "apikey"; $mail->Password = getenv("KOLTRIX_API_KEY"); $mail->setFrom("hello@acme.com"); $mail->addAddress("lead@example.com"); $mail->Subject = "Hello via SMTP"; $mail->isHTML(true); $mail->Body = "It works.
"; // leave AltBody empty: one part only $mail->send(); ``` ## A session, line by line ```text S: 220 koltrix-smtp ready C: EHLO app.acme.com S: 250-koltrix-smtp S: 250-AUTH PLAIN LOGIN S: 250-8BITMIME S: 250-SIZE 26214400 S: 250 HELP C: AUTH PLAIN AGFwaWtleQBreF8uLi4= S: 235 2.7.0 Authentication successful C: MAIL FROM:Hi {{first_name|there}},
This month we shipped…
", "preheader": "Faster search and a new inbox" }' ``` ```json { "campaign_id": "1d2c3b4a-5f6e-4d7c-8b9a-0f1e2d3c4b5a", "status": "queued", "message": "Broadcast enqueued — recipients will be fanned out by the worker." } ``` **Body fields** | Name | Required | Description | | --- | --- | --- | | `subject` | Yes | Sent as written; merge variables aren't filled in here. | | `body_html` | Yes | The HTML body. | | `body_text` | No | Generated from the HTML when omitted. | | `preheader` | No | The preview text most inboxes show after the subject. | | `from_name, from_address` | No | Default to the list's. `from_address` is required if the list has none. | Each active subscriber gets their own copy with: - an unsubscribe link at the end and a `List-Unsubscribe` header, so mail providers can show their own Unsubscribe button, - open tracking, and links rewritten for click tracking, - the merge variables below filled in. A broadcast from a new domain is paced by its daily sending cap; one that would go over today's cap is paused. See [Deliverability](https://docs.koltrix.com/deliverability.md#new-domains). Each copy appears in your message log (`GET /api/v2/messages`) and fires [webhooks](https://docs.koltrix.com/webhooks.md) like any other send. ## Merge variables These are filled in per recipient in broadcast **bodies**: | Variable | Becomes | | --- | --- | | `{{name}}` | The subscriber's name, or nothing. | | `{{first_name}}` | The first word of the name, or nothing. | | `{{first_name\|there}}` | The first name, or the fallback after the bar (here, "there"). | | `{{email}}` | The subscriber's address. | | `{{unsubscribe_url}}` | Their personal unsubscribe link. | In HTML bodies, names are HTML-escaped, so a subscriber can't inject markup into your email. A variable Koltrix doesn't recognise is left exactly as written. Merge variables work in broadcasts and [sequences](https://docs.koltrix.com/sequences.md), but **not** in `POST /api/v2/emails`, which sends your text unchanged. ## Signup forms Embedded signup forms need the Newsletters screen to give you a form's address, and that screen isn't available in this release. Until it returns, add people from your own backend with `POST /api/v2/lists/:id/subscribers` after they sign up on your site. --- # Sequences Source: https://docs.koltrix.com/sequences > **API only for now** > > Sequences has no dashboard screen in this release. Everything on this page works over the API exactly as documented, and existing integrations are unaffected — we do not change or remove a /api/v2 endpoint when a screen is hidden. The UI returns in a later release. A **sequence** is a series of emails sent to each new subscriber of a [list](https://docs.koltrix.com/newsletters.md), on a schedule that starts when they join: a welcome email straight away, a setup guide a day later, a check-in three days after that. Koltrix schedules every step for every subscriber; you don't run a cron job. Every endpoint is in the [API reference](https://docs.koltrix.com/api-reference.md#group-sequences). Reading needs `read` or `newsletter`; changes need `newsletter`. ## Create a sequence ```bash curl https://api.koltrix.com/api/v2/sequences \ -H "Authorization: Bearer $KOLTRIX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Welcome series", "list_id": "0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d" }' ``` ```json { "id": "3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9", "list_id": "0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", "name": "Welcome series", "trigger": "subscribe", "active": true, "step_count": 0, "created_at": "2026-10-02T12:00:00Z", "updated_at": "2026-10-02T12:00:00Z" } ``` `trigger` is `subscribe`, the only trigger: a person is enrolled when they become an active subscriber of the list. New sequences are active. ## Add steps Each step has a `position` (starting at 0), a `delay_hours` counted from the previous step (from enrolment for step 0), a `subject` and a body. ```bash curl https://api.koltrix.com/api/v2/sequences/$SEQ_ID/steps \ -H "Authorization: Bearer $KOLTRIX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "position": 0, "delay_hours": 0, "subject": "Welcome to Acme", "body_html": "Hi {{first_name|there}}, glad you are here.
" }' curl https://api.koltrix.com/api/v2/sequences/$SEQ_ID/steps \ -H "Authorization: Bearer $KOLTRIX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "position": 1, "delay_hours": 24, "subject": "Your five-minute setup", "body_html": "Here is the setup guide we promised.
" }' curl https://api.koltrix.com/api/v2/sequences/$SEQ_ID/steps \ -H "Authorization: Bearer $KOLTRIX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "position": 2, "delay_hours": 72, "subject": "How is it going?", "body_html": "Anything I can help with? Just reply.
" }' ``` Each delay counts from the step before it, so this sends at 0 hours, 24 hours and 96 hours after someone joins. In step bodies, `{{email}}` and `{{unsubscribe_url}}` are filled in. The name variables are always empty in sequences, so give `{{first_name}}` a fallback, as above. The subject is sent as written. ## Inspect and delete | Action | Request | | --- | --- | | List sequences | `GET /api/v2/sequences` | | List a sequence's steps | `GET /api/v2/sequences/:id/steps` | | Delete a step | `DELETE /api/v2/sequences/:id/steps/:step_id` | | Delete a sequence and its steps | `DELETE /api/v2/sequences/:id` | Deletes answer `204`. Steps can't be edited: delete one and add a new one in its place. ## How a subscriber moves through it When someone becomes an active subscriber, by `POST /api/v2/lists/:id/subscribers` or by confirming a double opt-in, Koltrix enrols them in every active sequence on the list and schedules step 0. After each step it schedules the next, until the last. Before each step Koltrix checks: 1. **Still subscribed?** Someone who unsubscribed is skipped, and their enrolment ends. 2. **On the suppression list?** Then nothing is sent and the enrolment ends. 3. **Does the list have a From address?** If not, the enrolment is paused until you set `from_address` on the list. 4. **Within today's sending cap for a new domain?** If not, the step waits until tomorrow. Each step goes out with an unsubscribe link and a `List-Unsubscribe` header. Unsubscribing at any point stops the rest of the sequence. Subscribers added by CSV import aren't enrolled, and nor are people who were already on the list when you created the sequence. > **Current limitation** > > Sequence emails aren't recorded in the message log > and don't fire webhooks, and they have no open or click tracking. ## Limits Delays are whole hours. For "send in 30 seconds", use `POST /api/v2/emails`. --- # Receiving replies Source: https://docs.koltrix.com/replies Most transactional email is sent from `noreply@` and every answer is thrown away. Koltrix works the other way round: a reply to a receipt, a password reset or a shipping notice comes back to your **team inbox**, where someone can answer it, and it is counted on the message it replied to. You don't have to switch anything on. ## Mail sent with the API or the SMTP relay The From address of every API and SMTP send is a real mailbox in your workspace (that is why it has to be [registered](https://docs.koltrix.com/sending-email.md#before-you-send)). So when someone presses Reply: 1. The reply goes to your From address and arrives in your team inbox, in that mailbox, for whoever has access to it. 2. It is threaded with the message you sent, which is in the mailbox's Sent folder. 3. Koltrix matches the reply's `In-Reply-To` and `References` headers to the message and records it: `reply_count` goes up, `replied_at` is set the first time, `engagement_score` goes up by 10, and a `reply` event is added to the message's timeline. ```bash curl https://api.koltrix.com/api/v2/messages/6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20 \ -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` ```json { "id": "6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20", "to": "lead@example.com", "subject": "Your receipt for order 1042", "status": "sent", "reply_count": 1, "replied_at": "2026-10-02T15:12:40Z", "engagement_score": 12 } ``` (Abridged; the full response is in [Sending email](https://docs.koltrix.com/sending-email.md#checking-a-message).) A reply is the most reliable engagement signal there is. An open can be triggered by a mail provider fetching images, and a click by a link scanner; a reply needs a person. If replies should go somewhere else, such as an existing helpdesk, send from an address whose replies you forward there, or set up **Settings → Forwarding** for that mailbox. ## Mail sent from the Koltrix app Messages your team sends from the Koltrix app also carry a **signed reply address** in their `Reply-To` header: ```text Reply-To: reply+Thanks for your order.
", "body_text": "Thanks for your order." }' ``` **Node** ```js import { randomUUID } from "node:crypto"; const res = await fetch("https://api.koltrix.com/api/v2/emails", { method: "POST", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": randomUUID(), }, body: JSON.stringify({ "from": "hello@acme.com", "to": [ "lead@example.com" ], "subject": "Your receipt for order 1042", "body_html": "Thanks for your order.
", "body_text": "Thanks for your order." }), }); console.log(res.status, await res.json()); ``` **Python** ```python import os, uuid, requests res = requests.post( "https://api.koltrix.com/api/v2/emails", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", "Idempotency-Key": str(uuid.uuid4()), }, json={ "from": "hello@acme.com", "to": [ "lead@example.com" ], "subject": "Your receipt for order 1042", "body_html": "Thanks for your order.
", "body_text": "Thanks for your order." }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "strings" "fmt" "io" "net/http" "os" "github.com/google/uuid" ) func main() { body := strings.NewReader(`{ "from": "hello@acme.com", "to": [ "lead@example.com" ], "subject": "Your receipt for order 1042", "body_html": "Thanks for your order.
", "body_text": "Thanks for your order." }`) req, _ := http.NewRequest("POST", "https://api.koltrix.com/api/v2/emails", body) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) req.Header.Set("Idempotency-Key", uuid.NewString()) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), "Content-Type: application/json", "Idempotency-Key: " . bin2hex(random_bytes(16)), ], CURLOPT_POSTFIELDS => json_encode([ "from" => "hello@acme.com", "to" => [ "lead@example.com", ], "subject" => "Your receipt for order 1042", "body_html" => "Thanks for your order.
", "body_text" => "Thanks for your order.", ]), ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 202 Accepted Accepted and queued. ```json { "id": "6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20", "status": "queued", "queued": true, "tracking_url": "/api/v2/messages/6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20", "recipient_count": 1 } ``` ### 200 OK (replay) A retry with the same Idempotency-Key after the first request finished. The original response, not a second send. Headers: `Idempotent-Replayed: true` ```json { "id": "6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20", "status": "queued", "queued": true, "tracking_url": "/api/v2/messages/6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20", "recipient_count": 1 } ``` ### 400 Bad Request A required field is missing or invalid. The text names the problem. ```json { "error": "from, to, subject are required" } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the send scope. ```json { "error": "missing permission: send" } ``` ### 403 Forbidden (From address) The From address is not an active address in this workspace. The message says whether the domain is verified. ```json { "error": "from address 'hello@acme.com' is not registered for this account. The domain is verified — add this exact address in Dashboard → Domains." } ``` ### 409 Conflict A retry arrived while the first request with this Idempotency-Key was still running. Wait a moment and retry with the same key. ```json { "error": "a request with this Idempotency-Key is still in progress" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` ### 429 Too Many Requests (quota) Your plan's send quota is used up. Retrying will not help until resets_at; tell a person. ```json { "code": "quota_exceeded", "error": "You've hit today's API sending limit. It resets at midnight UTC, or upgrade to send more now.", "kind": "api_sends", "limit": 200, "used": 200, "window": "day", "resets_at": "2026-10-03T00:00:00Z", "upgrade_url": "https://app.koltrix.com/settings/billing" } ``` ### 503 Service Unavailable Koltrix could not check the Idempotency-Key, so it sent nothing. Retry with the same key. ```json { "error": "idempotency store unavailable, please retry" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # List messages Source: https://docs.koltrix.com/api-reference/list-messages **GET** `https://api.koltrix.com/api/v2/messages` Newest first. `status` is one of `queued`, `sent` or `bounced`. Resource: Messages. Requires an API key with the `read` or `send` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `status` | query | `string` | No | | Only messages with this status: `queued`, `sent` or `bounced`. One of: `queued`, `sent`, `bounced`. | | `recipient` | query | `string` | No | | Only messages whose recipient contains this text (case-insensitive). | | `limit` | query | `number` | No | `100` | Page size. Default 100, maximum 500. | | `offset` | query | `number` | No | | How many messages to skip. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X GET "https://api.koltrix.com/api/v2/messages?status=sent&recipient=example.com&limit=50&offset=0" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/messages?status=sent&recipient=example.com&limit=50&offset=0", { method: "GET", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, }, }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.get( "https://api.koltrix.com/api/v2/messages?status=sent&recipient=example.com&limit=50&offset=0", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "fmt" "io" "net/http" "os" ) func main() { req, _ := http.NewRequest("GET", "https://api.koltrix.com/api/v2/messages?status=sent&recipient=example.com&limit=50&offset=0", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), ], ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 200 OK Success. ```json { "data": [ { "id": "6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20", "tenant_id": "5c4b3a29-1807-4f6e-9d5c-4b3a29180716", "message_id": "0b9e6f4c-8a1d-4f2e-b7c3-5d6e7f8a9b0c@acme.com", "from": "hello@acme.com", "to": "lead@example.com", "subject": "Your receipt for order 1042", "status": "sent", "open_count": 2, "click_count": 0, "created_at": "2026-10-02T14:00:00Z", "sent_at": "2026-10-02T14:00:01Z", "opened_at": "2026-10-02T14:07:04Z" } ], "total": 1 } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the read scope. ```json { "error": "missing permission: read" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # Get a message Source: https://docs.koltrix.com/api-reference/get-message **GET** `https://api.koltrix.com/api/v2/messages/:id` One message with its status, engagement counters and timestamps. `engagement_score` adds 1 per open, 5 per click and 10 per reply. Currently answers 404 while the message is still queued, usually for under a second. Resource: Messages. Requires an API key with the `read` or `send` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `id` | path | `string` | Yes | | The message id returned by `POST /api/v2/emails`. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X GET "https://api.koltrix.com/api/v2/messages/6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/messages/6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20", { method: "GET", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, }, }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.get( "https://api.koltrix.com/api/v2/messages/6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "fmt" "io" "net/http" "os" ) func main() { req, _ := http.NewRequest("GET", "https://api.koltrix.com/api/v2/messages/6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), ], ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 200 OK Success. ```json { "id": "6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20", "from": "hello@acme.com", "to": "lead@example.com", "subject": "Your receipt for order 1042", "status": "sent", "open_count": 2, "click_count": 0, "reply_count": 1, "engagement_score": 12, "first_open_after_seconds": 423, "message_id": "0b9e6f4c-8a1d-4f2e-b7c3-5d6e7f8a9b0c@acme.com", "error": null, "created_at": "2026-10-02T14:00:00Z", "sent_at": "2026-10-02T14:00:01Z", "opened_at": "2026-10-02T14:07:04Z", "clicked_at": null, "replied_at": "2026-10-02T15:12:40Z", "bounced_at": null } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the read scope. ```json { "error": "missing permission: read" } ``` ### 404 Not Found No such record in this workspace. ```json { "error": "message not found" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # List a message's events Source: https://docs.koltrix.com/api-reference/get-message-events **GET** `https://api.koltrix.com/api/v2/messages/:id/events` Every open, click and reply, oldest first. `kind` is `open`, `click` or `reply`; a click also has the `url`. `prefetch: true` marks an open that looks like automatic image fetching by a mail provider; those are not counted in `open_count`. Resource: Messages. Requires an API key with the `read` or `send` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `id` | path | `string` | Yes | | The message id. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X GET "https://api.koltrix.com/api/v2/messages/6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20/events" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/messages/6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20/events", { method: "GET", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, }, }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.get( "https://api.koltrix.com/api/v2/messages/6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20/events", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "fmt" "io" "net/http" "os" ) func main() { req, _ := http.NewRequest("GET", "https://api.koltrix.com/api/v2/messages/6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20/events", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), ], ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 200 OK Success. ```json { "data": [ { "kind": "open", "prefetch": true, "ip": "17.58.0.12", "user_agent": "Mozilla/5.0", "created_at": "2026-10-02T14:00:03Z" }, { "kind": "open", "prefetch": false, "ip": "203.0.113.7", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5)", "created_at": "2026-10-02T14:07:04Z" }, { "kind": "reply", "prefetch": false, "user_agent": "inbound-mail", "created_at": "2026-10-02T15:12:40Z" } ], "total": 3 } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the read scope. ```json { "error": "missing permission: read" } ``` ### 404 Not Found No such record in this workspace. ```json { "error": "message not found" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # Get the current key Source: https://docs.koltrix.com/api-reference/get-me **GET** `https://api.koltrix.com/api/v2/me` Returns the workspace short name (`tenant`) and the key's scopes. Works with any valid key, so it makes a good health check. Resource: Your key. Works with any valid API key. ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X GET "https://api.koltrix.com/api/v2/me" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/me", { method: "GET", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, }, }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.get( "https://api.koltrix.com/api/v2/me", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "fmt" "io" "net/http" "os" ) func main() { req, _ := http.NewRequest("GET", "https://api.koltrix.com/api/v2/me", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), ], ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 200 OK Success. ```json { "tenant": "acme", "permissions": [ "read", "send" ] } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # List lists Source: https://docs.koltrix.com/api-reference/list-lists **GET** `https://api.koltrix.com/api/v2/lists` Every list in the workspace. Resource: Lists. Requires an API key with the `read` or `newsletter` scope. ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X GET "https://api.koltrix.com/api/v2/lists" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/lists", { method: "GET", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, }, }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.get( "https://api.koltrix.com/api/v2/lists", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "fmt" "io" "net/http" "os" ) func main() { req, _ := http.NewRequest("GET", "https://api.koltrix.com/api/v2/lists", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), ], ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 200 OK Success. ```json { "data": [ { "id": "0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", "name": "Product updates", "description": "What shipped this month", "from_name": "Acme", "from_address": "hello@acme.com", "reply_to": "", "subscriber_count": 1284, "created_at": "2026-09-12T09:14:22Z", "updated_at": "2026-10-01T08:00:00Z" } ] } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the newsletter scope. ```json { "error": "missing permission: newsletter" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # Create a list Source: https://docs.koltrix.com/api-reference/create-list **POST** `https://api.koltrix.com/api/v2/lists` Returns 201 with the new list. `from_address` is the default sender for broadcasts and sequences on the list; use an active address on a verified domain. Resource: Lists. Requires an API key with the `newsletter` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `name` | body | `string` | Yes | | The list's name. | | `description` | body | `string` | No | | A note for your team. | | `from_name` | body | `string` | No | | Default sender name. | | `from_address` | body | `string` | No | | Default sender address. | | `reply_to` | body | `string` | No | | Stored with the list. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X POST "https://api.koltrix.com/api/v2/lists" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Product updates", "description": "What shipped this month", "from_name": "Acme", "from_address": "hello@acme.com" }' ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/lists", { method: "POST", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "name": "Product updates", "description": "What shipped this month", "from_name": "Acme", "from_address": "hello@acme.com" }), }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.post( "https://api.koltrix.com/api/v2/lists", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, json={ "name": "Product updates", "description": "What shipped this month", "from_name": "Acme", "from_address": "hello@acme.com" }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "strings" "fmt" "io" "net/http" "os" ) func main() { body := strings.NewReader(`{ "name": "Product updates", "description": "What shipped this month", "from_name": "Acme", "from_address": "hello@acme.com" }`) req, _ := http.NewRequest("POST", "https://api.koltrix.com/api/v2/lists", body) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "name" => "Product updates", "description" => "What shipped this month", "from_name" => "Acme", "from_address" => "hello@acme.com", ]), ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 201 Created Created. ```json { "id": "0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", "name": "Product updates", "description": "What shipped this month", "from_name": "Acme", "from_address": "hello@acme.com", "reply_to": "", "subscriber_count": 0, "created_at": "2026-10-02T10:11:12Z", "updated_at": "2026-10-02T10:11:12Z" } ``` ### 400 Bad Request A required field is missing or invalid. The text names the problem. ```json { "error": "name is required" } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the newsletter scope. ```json { "error": "missing permission: newsletter" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # Get a list Source: https://docs.koltrix.com/api-reference/get-list **GET** `https://api.koltrix.com/api/v2/lists/:id` One list. 404 if there is no such list. Resource: Lists. Requires an API key with the `read` or `newsletter` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `id` | path | `string` | Yes | | The list id. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X GET "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", { method: "GET", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, }, }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.get( "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "fmt" "io" "net/http" "os" ) func main() { req, _ := http.NewRequest("GET", "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), ], ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 200 OK Success. ```json { "id": "0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", "name": "Product updates", "description": "What shipped this month", "from_name": "Acme", "from_address": "hello@acme.com", "reply_to": "", "subscriber_count": 1284, "created_at": "2026-09-12T09:14:22Z", "updated_at": "2026-10-01T08:00:00Z" } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the newsletter scope. ```json { "error": "missing permission: newsletter" } ``` ### 404 Not Found No such record in this workspace. ```json { "error": "list not found" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # Update a list Source: https://docs.koltrix.com/api-reference/update-list **PATCH** `https://api.koltrix.com/api/v2/lists/:id` Changes only the fields you send and returns the updated list. Resource: Lists. Requires an API key with the `newsletter` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `id` | path | `string` | Yes | | The list id. | | `name` | body | `string` | No | | New name. | | `description` | body | `string` | No | | New description. | | `from_name` | body | `string` | No | | New default sender name. | | `from_address` | body | `string` | No | | New default sender address. | | `reply_to` | body | `string` | No | | New reply-to value. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X PATCH "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Changelog" }' ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", { method: "PATCH", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "name": "Changelog" }), }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.patch( "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, json={ "name": "Changelog" }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "strings" "fmt" "io" "net/http" "os" ) func main() { body := strings.NewReader(`{ "name": "Changelog" }`) req, _ := http.NewRequest("PATCH", "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", body) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "PATCH", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "name" => "Changelog", ]), ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 200 OK Success. ```json { "id": "0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", "name": "Changelog", "description": "What shipped this month", "from_name": "Acme", "from_address": "hello@acme.com", "reply_to": "", "subscriber_count": 1284, "created_at": "2026-09-12T09:14:22Z", "updated_at": "2026-10-02T10:12:00Z" } ``` ### 400 Bad Request The body is not valid JSON, or a field has the wrong type. ```json { "error": "invalid body" } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the newsletter scope. ```json { "error": "missing permission: newsletter" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # Delete a list Source: https://docs.koltrix.com/api-reference/delete-list **DELETE** `https://api.koltrix.com/api/v2/lists/:id` Deletes the list and its subscribers. Cannot be undone. Returns 204 with an empty body. Resource: Lists. Requires an API key with the `newsletter` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `id` | path | `string` | Yes | | The list id. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X DELETE "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, }, }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.delete( "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "fmt" "io" "net/http" "os" ) func main() { req, _ := http.NewRequest("DELETE", "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "DELETE", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), ], ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 204 No Content Done. The body is empty. Empty body. ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the newsletter scope. ```json { "error": "missing permission: newsletter" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # List subscribers Source: https://docs.koltrix.com/api-reference/list-subscribers **GET** `https://api.koltrix.com/api/v2/lists/:id/subscribers` `status` is one of `pending` (waiting for double opt-in), `active` or `unsubscribed`. Resource: Subscribers. Requires an API key with the `read` or `newsletter` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `id` | path | `string` | Yes | | The list id. | | `status` | query | `string` | No | | Only subscribers with this status. One of: `pending`, `active`, `unsubscribed`. | | `limit` | query | `number` | No | `100` | Page size. Default 100. | | `offset` | query | `number` | No | | How many to skip. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X GET "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers?status=active&limit=100&offset=0" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers?status=active&limit=100&offset=0", { method: "GET", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, }, }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.get( "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers?status=active&limit=100&offset=0", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "fmt" "io" "net/http" "os" ) func main() { req, _ := http.NewRequest("GET", "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers?status=active&limit=100&offset=0", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), ], ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 200 OK Success. ```json { "data": [ { "id": "9d8c7b6a-5f4e-4d3c-b2a1-0f9e8d7c6b5a", "list_id": "0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", "email": "ada@example.com", "name": "Ada Lovelace", "status": "active", "unsubscribe_token": "4f9a0c1d2e3b4a5f6c7d8e9f0a1b2c3d", "tags": [ "beta" ], "created_at": "2026-09-20T14:01:00Z", "updated_at": "2026-09-20T14:01:00Z" } ], "total": 1 } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the newsletter scope. ```json { "error": "missing permission: newsletter" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # Add a subscriber Source: https://docs.koltrix.com/api-reference/create-subscriber **POST** `https://api.koltrix.com/api/v2/lists/:id/subscribers` Returns 201 with the subscriber. Adding an email that is already on the list updates its name instead of failing; someone who unsubscribed stays unsubscribed. On a list that requires double opt-in the subscriber starts as `pending`. Resource: Subscribers. Requires an API key with the `newsletter` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `id` | path | `string` | Yes | | The list id. | | `email` | body | `string` | Yes | | The address. Stored lower-cased. | | `name` | body | `string` | No | | The person's name, used by the name merge variables in broadcasts. | | `tags` | body | `array` | No | | Array of strings. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X POST "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "ada@example.com", "name": "Ada Lovelace", "tags": [ "beta" ] }' ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers", { method: "POST", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "email": "ada@example.com", "name": "Ada Lovelace", "tags": [ "beta" ] }), }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.post( "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, json={ "email": "ada@example.com", "name": "Ada Lovelace", "tags": [ "beta" ] }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "strings" "fmt" "io" "net/http" "os" ) func main() { body := strings.NewReader(`{ "email": "ada@example.com", "name": "Ada Lovelace", "tags": [ "beta" ] }`) req, _ := http.NewRequest("POST", "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers", body) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "email" => "ada@example.com", "name" => "Ada Lovelace", "tags" => [ "beta", ], ]), ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 201 Created Created. ```json { "id": "9d8c7b6a-5f4e-4d3c-b2a1-0f9e8d7c6b5a", "list_id": "0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", "email": "ada@example.com", "name": "Ada Lovelace", "status": "active", "unsubscribe_token": "4f9a0c1d2e3b4a5f6c7d8e9f0a1b2c3d", "tags": [ "beta" ], "created_at": "2026-10-02T10:14:33Z", "updated_at": "2026-10-02T10:14:33Z" } ``` ### 400 Bad Request A required field is missing or invalid. The text names the problem. ```json { "error": "invalid email: …" } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the newsletter scope. ```json { "error": "missing permission: newsletter" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # Import subscribers from CSV Source: https://docs.koltrix.com/api-reference/import-subscribers **POST** `https://api.koltrix.com/api/v2/lists/:id/subscribers/import` Send the CSV as a string. A header row with `email`, `name` (or `first_name` or `full_name`) and `tags` columns is recognised; without one, the first column is the email. Separate tags with semicolons. Every imported row becomes `active`, even on a list that requires double opt-in, so import only people who agreed to hear from you. Addresses on your suppression list are still never mailed. Rows with an invalid email are skipped. Resource: Subscribers. Requires an API key with the `newsletter` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `id` | path | `string` | Yes | | The list id. | | `csv` | body | `string` | Yes | | The CSV file's contents. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X POST "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers/import" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "csv": "email,name,tags\nada@example.com,Ada Lovelace,beta;vip\nlinus@example.com,Linus,beta" }' ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers/import", { method: "POST", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "csv": "email,name,tags\nada@example.com,Ada Lovelace,beta;vip\nlinus@example.com,Linus,beta" }), }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.post( "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers/import", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, json={ "csv": "email,name,tags\nada@example.com,Ada Lovelace,beta;vip\nlinus@example.com,Linus,beta" }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "strings" "fmt" "io" "net/http" "os" ) func main() { body := strings.NewReader(`{ "csv": "email,name,tags\nada@example.com,Ada Lovelace,beta;vip\nlinus@example.com,Linus,beta" }`) req, _ := http.NewRequest("POST", "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers/import", body) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "csv" => "email,name,tags ada@example.com,Ada Lovelace,beta;vip linus@example.com,Linus,beta", ]), ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 200 OK Success. ```json { "imported": 2, "skipped": 0, "errors": null } ``` ### 400 Bad Request A required field is missing or invalid. The text names the problem. ```json { "error": "csv body required" } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the newsletter scope. ```json { "error": "missing permission: newsletter" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # Delete a subscriber Source: https://docs.koltrix.com/api-reference/delete-subscriber **DELETE** `https://api.koltrix.com/api/v2/lists/:id/subscribers/:sub_id` Removes the subscriber from the list. To stop mailing someone but keep the record, unsubscribe them instead. Returns 204 with an empty body. Resource: Subscribers. Requires an API key with the `newsletter` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `id` | path | `string` | Yes | | The list id. | | `sub_id` | path | `string` | Yes | | The subscriber id. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X DELETE "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers/9d8c7b6a-5f4e-4d3c-b2a1-0f9e8d7c6b5a" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers/9d8c7b6a-5f4e-4d3c-b2a1-0f9e8d7c6b5a", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, }, }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.delete( "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers/9d8c7b6a-5f4e-4d3c-b2a1-0f9e8d7c6b5a", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "fmt" "io" "net/http" "os" ) func main() { req, _ := http.NewRequest("DELETE", "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers/9d8c7b6a-5f4e-4d3c-b2a1-0f9e8d7c6b5a", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "DELETE", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), ], ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 204 No Content Done. The body is empty. Empty body. ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the newsletter scope. ```json { "error": "missing permission: newsletter" } ``` ### 404 Not Found No such record in this workspace. ```json { "error": "subscriber not found" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # Unsubscribe a subscriber Source: https://docs.koltrix.com/api-reference/unsubscribe-subscriber **POST** `https://api.koltrix.com/api/v2/lists/:id/subscribers/:sub_id/unsubscribe` Marks the subscriber `unsubscribed`, stops any sequence they are in and adds the address to your suppression list, exactly as if they had clicked the unsubscribe link. No body. Resource: Subscribers. Requires an API key with the `newsletter` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `id` | path | `string` | Yes | | The list id. | | `sub_id` | path | `string` | Yes | | The subscriber id. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X POST "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers/9d8c7b6a-5f4e-4d3c-b2a1-0f9e8d7c6b5a/unsubscribe" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers/9d8c7b6a-5f4e-4d3c-b2a1-0f9e8d7c6b5a/unsubscribe", { method: "POST", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, }, }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.post( "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers/9d8c7b6a-5f4e-4d3c-b2a1-0f9e8d7c6b5a/unsubscribe", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "fmt" "io" "net/http" "os" ) func main() { req, _ := http.NewRequest("POST", "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/subscribers/9d8c7b6a-5f4e-4d3c-b2a1-0f9e8d7c6b5a/unsubscribe", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), ], ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 200 OK Success. ```json { "status": "unsubscribed" } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the newsletter scope. ```json { "error": "missing permission: newsletter" } ``` ### 404 Not Found No such record in this workspace. ```json { "error": "subscriber not found" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # Send a broadcast Source: https://docs.koltrix.com/api-reference/send-broadcast **POST** `https://api.koltrix.com/api/v2/lists/:id/broadcast` Creates a campaign and queues it straight away; returns 202. Each subscriber gets their own copy with an unsubscribe link and header, open tracking and tracked links. In the body, the merge variables {{name}}, {{first_name}}, {{first_name|fallback}}, {{email}} and {{unsubscribe_url}} are filled in per recipient (the subject is sent as written). `from_address` and `from_name` default to the list's. Resource: Broadcasts. Requires an API key with the `newsletter` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `id` | path | `string` | Yes | | The list id. | | `subject` | body | `string` | Yes | | Subject line. | | `body_html` | body | `string` | Yes | | HTML body. | | `body_text` | body | `string` | No | | Plain-text body. Generated from the HTML when omitted. | | `preheader` | body | `string` | No | | Preview text shown after the subject in most inboxes. | | `from_name` | body | `string` | No | | Sender name. Defaults to the list's. | | `from_address` | body | `string` | No | | Sender address. Defaults to the list's; required if the list has none. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X POST "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/broadcast" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "subject": "What shipped in October", "body_html": "Hi {{first_name|there}},
This month we shipped…
", "body_text": "Hi {{first_name|there}}, this month we shipped…", "preheader": "Faster search and a new inbox" }' ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/broadcast", { method: "POST", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "subject": "What shipped in October", "body_html": "Hi {{first_name|there}},
This month we shipped…
", "body_text": "Hi {{first_name|there}}, this month we shipped…", "preheader": "Faster search and a new inbox" }), }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.post( "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/broadcast", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, json={ "subject": "What shipped in October", "body_html": "Hi {{first_name|there}},
This month we shipped…
", "body_text": "Hi {{first_name|there}}, this month we shipped…", "preheader": "Faster search and a new inbox" }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "strings" "fmt" "io" "net/http" "os" ) func main() { body := strings.NewReader(`{ "subject": "What shipped in October", "body_html": "Hi {{first_name|there}},
This month we shipped…
", "body_text": "Hi {{first_name|there}}, this month we shipped…", "preheader": "Faster search and a new inbox" }`) req, _ := http.NewRequest("POST", "https://api.koltrix.com/api/v2/lists/0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d/broadcast", body) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "subject" => "What shipped in October", "body_html" => "Hi {{first_name|there}},
This month we shipped…
", "body_text" => "Hi {{first_name|there}}, this month we shipped…", "preheader" => "Faster search and a new inbox", ]), ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 202 Accepted Accepted and queued. ```json { "campaign_id": "1d2c3b4a-5f6e-4d7c-8b9a-0f1e2d3c4b5a", "status": "queued", "message": "Broadcast enqueued — recipients will be fanned out by the worker." } ``` ### 400 Bad Request A required field is missing or invalid. The text names the problem. ```json { "error": "subject and body_html required" } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the newsletter scope. ```json { "error": "missing permission: newsletter" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # List sequences Source: https://docs.koltrix.com/api-reference/list-sequences **GET** `https://api.koltrix.com/api/v2/sequences` Every sequence in the workspace, with its number of steps. Resource: Sequences. Requires an API key with the `read` or `newsletter` scope. ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X GET "https://api.koltrix.com/api/v2/sequences" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/sequences", { method: "GET", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, }, }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.get( "https://api.koltrix.com/api/v2/sequences", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "fmt" "io" "net/http" "os" ) func main() { req, _ := http.NewRequest("GET", "https://api.koltrix.com/api/v2/sequences", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), ], ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 200 OK Success. ```json { "data": [ { "id": "3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9", "list_id": "0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", "name": "Welcome series", "trigger": "subscribe", "active": true, "step_count": 3, "created_at": "2026-09-21T12:00:00Z", "updated_at": "2026-09-21T12:00:00Z" } ] } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the newsletter scope. ```json { "error": "missing permission: newsletter" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # Create a sequence Source: https://docs.koltrix.com/api-reference/create-sequence **POST** `https://api.koltrix.com/api/v2/sequences` Returns 201. `trigger` defaults to `subscribe`, the only trigger: people are enrolled when they become active on the list. Resource: Sequences. Requires an API key with the `newsletter` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `name` | body | `string` | Yes | | The sequence's name. | | `list_id` | body | `string` | Yes | | The list whose new subscribers are enrolled. | | `trigger` | body | `string` | No | `subscribe` | `subscribe` (the default and only value). One of: `subscribe`. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X POST "https://api.koltrix.com/api/v2/sequences" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Welcome series", "list_id": "0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", "trigger": "subscribe" }' ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/sequences", { method: "POST", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "name": "Welcome series", "list_id": "0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", "trigger": "subscribe" }), }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.post( "https://api.koltrix.com/api/v2/sequences", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, json={ "name": "Welcome series", "list_id": "0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", "trigger": "subscribe" }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "strings" "fmt" "io" "net/http" "os" ) func main() { body := strings.NewReader(`{ "name": "Welcome series", "list_id": "0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", "trigger": "subscribe" }`) req, _ := http.NewRequest("POST", "https://api.koltrix.com/api/v2/sequences", body) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "name" => "Welcome series", "list_id" => "0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", "trigger" => "subscribe", ]), ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 201 Created Created. ```json { "id": "3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9", "list_id": "0b3c6d2e-91f4-4a7b-8c5d-2e3f4a5b6c7d", "name": "Welcome series", "trigger": "subscribe", "active": true, "step_count": 0, "created_at": "2026-10-02T12:00:00Z", "updated_at": "2026-10-02T12:00:00Z" } ``` ### 400 Bad Request A required field is missing or invalid. The text names the problem. ```json { "error": "name is required" } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the newsletter scope. ```json { "error": "missing permission: newsletter" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # Delete a sequence Source: https://docs.koltrix.com/api-reference/delete-sequence **DELETE** `https://api.koltrix.com/api/v2/sequences/:id` Deletes the sequence and its steps. Returns 204 with an empty body. Resource: Sequences. Requires an API key with the `newsletter` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `id` | path | `string` | Yes | | The sequence id. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X DELETE "https://api.koltrix.com/api/v2/sequences/3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/sequences/3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, }, }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.delete( "https://api.koltrix.com/api/v2/sequences/3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "fmt" "io" "net/http" "os" ) func main() { req, _ := http.NewRequest("DELETE", "https://api.koltrix.com/api/v2/sequences/3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "DELETE", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), ], ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 204 No Content Done. The body is empty. Empty body. ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the newsletter scope. ```json { "error": "missing permission: newsletter" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # List a sequence's steps Source: https://docs.koltrix.com/api-reference/list-sequence-steps **GET** `https://api.koltrix.com/api/v2/sequences/:id/steps` The steps in order of `position`. Resource: Sequences. Requires an API key with the `read` or `newsletter` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `id` | path | `string` | Yes | | The sequence id. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X GET "https://api.koltrix.com/api/v2/sequences/3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9/steps" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/sequences/3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9/steps", { method: "GET", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, }, }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.get( "https://api.koltrix.com/api/v2/sequences/3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9/steps", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "fmt" "io" "net/http" "os" ) func main() { req, _ := http.NewRequest("GET", "https://api.koltrix.com/api/v2/sequences/3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9/steps", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "GET", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), ], ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 200 OK Success. ```json { "data": [ { "id": "7e6d5c4b-3a29-4f18-8e7d-6c5b4a392817", "sequence_id": "3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9", "position": 0, "delay_hours": 0, "subject": "Welcome to Acme", "body_html": "Hi {{first_name|there}}, glad you're here.
", "body_text": "Hi {{first_name|there}}, glad you're here.", "created_at": "2026-10-02T12:01:00Z" } ] } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the newsletter scope. ```json { "error": "missing permission: newsletter" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # Add a step Source: https://docs.koltrix.com/api-reference/add-sequence-step **POST** `https://api.koltrix.com/api/v2/sequences/:id/steps` Returns 201. `delay_hours` is counted from the previous step (from enrolment for the first step). In step bodies {{email}} and {{unsubscribe_url}} are filled in; the name variables are always empty, so give {{first_name}} a fallback. Resource: Sequences. Requires an API key with the `newsletter` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `id` | path | `string` | Yes | | The sequence id. | | `position` | body | `number` | Yes | | Order in the sequence, starting at 0. | | `delay_hours` | body | `number` | Yes | | Hours to wait after the previous step. | | `subject` | body | `string` | Yes | | Subject line. | | `body_html` | body | `string` | No | | HTML body. | | `body_text` | body | `string` | No | | Plain-text body. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X POST "https://api.koltrix.com/api/v2/sequences/3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9/steps" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "position": 0, "delay_hours": 0, "subject": "Welcome to Acme", "body_html": "Hi {{first_name|there}}, glad you'\''re here.
", "body_text": "Hi {{first_name|there}}, glad you'\''re here." }' ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/sequences/3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9/steps", { method: "POST", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ "position": 0, "delay_hours": 0, "subject": "Welcome to Acme", "body_html": "Hi {{first_name|there}}, glad you're here.
", "body_text": "Hi {{first_name|there}}, glad you're here." }), }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.post( "https://api.koltrix.com/api/v2/sequences/3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9/steps", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, json={ "position": 0, "delay_hours": 0, "subject": "Welcome to Acme", "body_html": "Hi {{first_name|there}}, glad you're here.
", "body_text": "Hi {{first_name|there}}, glad you're here." }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "strings" "fmt" "io" "net/http" "os" ) func main() { body := strings.NewReader(`{ "position": 0, "delay_hours": 0, "subject": "Welcome to Acme", "body_html": "Hi {{first_name|there}}, glad you're here.
", "body_text": "Hi {{first_name|there}}, glad you're here." }`) req, _ := http.NewRequest("POST", "https://api.koltrix.com/api/v2/sequences/3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9/steps", body) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "POST", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), "Content-Type: application/json", ], CURLOPT_POSTFIELDS => json_encode([ "position" => 0, "delay_hours" => 0, "subject" => "Welcome to Acme", "body_html" => "Hi {{first_name|there}}, glad you're here.
", "body_text" => "Hi {{first_name|there}}, glad you're here.", ]), ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 201 Created Created. ```json { "id": "7e6d5c4b-3a29-4f18-8e7d-6c5b4a392817", "sequence_id": "3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9", "position": 0, "delay_hours": 0, "subject": "Welcome to Acme", "body_html": "Hi {{first_name|there}}, glad you're here.
", "body_text": "Hi {{first_name|there}}, glad you're here.", "created_at": "2026-10-02T12:01:00Z" } ``` ### 400 Bad Request A required field is missing or invalid. The text names the problem. ```json { "error": "subject is required" } ``` ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the newsletter scope. ```json { "error": "missing permission: newsletter" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # Delete a step Source: https://docs.koltrix.com/api-reference/delete-sequence-step **DELETE** `https://api.koltrix.com/api/v2/sequences/:id/steps/:step_id` Returns 204 with an empty body. Resource: Sequences. Requires an API key with the `newsletter` scope. ## Parameters | Name | In | Type | Required | Default | Description | | --- | --- | --- | --- | --- | --- | | `id` | path | `string` | Yes | | The sequence id. | | `step_id` | path | `string` | Yes | | The step id. | ## Request examples Every example reads the API key from the environment variable `KOLTRIX_API_KEY`. **cURL** ```bash curl -X DELETE "https://api.koltrix.com/api/v2/sequences/3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9/steps/7e6d5c4b-3a29-4f18-8e7d-6c5b4a392817" \ -H "Authorization: Bearer $KOLTRIX_API_KEY" ``` **Node** ```js const res = await fetch("https://api.koltrix.com/api/v2/sequences/3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9/steps/7e6d5c4b-3a29-4f18-8e7d-6c5b4a392817", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, }, }); console.log(res.status, await res.json()); ``` **Python** ```python import os, requests res = requests.delete( "https://api.koltrix.com/api/v2/sequences/3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9/steps/7e6d5c4b-3a29-4f18-8e7d-6c5b4a392817", headers={ "Authorization": f"Bearer {os.environ['KOLTRIX_API_KEY']}", }, ) print(res.status_code, res.json()) ``` **Go** ```go package main import ( "fmt" "io" "net/http" "os" ) func main() { req, _ := http.NewRequest("DELETE", "https://api.koltrix.com/api/v2/sequences/3a2b1c0d-9e8f-4a7b-b6c5-d4e3f2a1b0c9/steps/7e6d5c4b-3a29-4f18-8e7d-6c5b4a392817", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("KOLTRIX_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close() out, _ := io.ReadAll(res.Body) fmt.Println(res.StatusCode, string(out)) } ``` **PHP** ```php "DELETE", CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer " . getenv("KOLTRIX_API_KEY"), ], ]); $response = curl_exec($ch); echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response; ``` ## Responses ### 204 No Content Done. The body is empty. Empty body. ### 401 Unauthorized The API key is missing, malformed, unknown or revoked. ```json { "error": "invalid or revoked api key" } ``` ### 403 Forbidden The key is valid but does not have the newsletter scope. ```json { "error": "missing permission: newsletter" } ``` ### 429 Too Many Requests (rate limit) More than 60 requests a minute on this key. Wait Retry-After seconds. No code field. Headers: `Retry-After: 60` ```json { "error": "rate limit exceeded — 60 requests per minute per API key" } ``` See also: [Errors](https://docs.koltrix.com/errors.md), [Authentication](https://docs.koltrix.com/authentication.md). --- # Errors Source: https://docs.koltrix.com/errors Errors from `https://api.koltrix.com/api/v2` are JSON with an `error` string: ```json { "error": "missing permission: send" } ``` The `error` text is written for a person and may change wording. Decide what to do from the **HTTP status**, and for `429` from the `code` field (see below). Successful responses use `200`, `201` (created), `202` (accepted for sending) and `204` (deleted, empty body). ## Should I retry? | Status | Retry? | | --- | --- | | `400`, `401`, `403`, `404` | No. Fix the request, key or data first. | | `409` | Yes, after a short wait, with the **same** `Idempotency-Key`. | | `429` without `code` | Yes, after `Retry-After` seconds. | | `429` with `"code": "quota_exceeded"` | Not until `resets_at`. Tell a person. | | `500` | Yes, with backoff. For `POST /api/v2/emails`, retry with the same `Idempotency-Key` and nothing is sent twice. | | `503` | Yes, with the same `Idempotency-Key`. | A good default: retry `409`, `429` (rate limit), `500` and `503` with exponential backoff starting at one second, give up after a few minutes, and log everything else for a person to look at. ## 400 Bad Request The request can't be processed as sent. | `error` | Endpoint | Fix | | --- | --- | --- | | `invalid body` | Any with a body | Send valid JSON with `Content-Type: application/json`, and check field types: `to`, `cc` and `bcc` are arrays. | | `from, to, subject are required` | `POST /emails` | Include all three, non-empty. | | `from address is not a valid email` | `POST /emails` | `from` must contain an address, like `hello@acme.com` or `AcmeThanks for your order of ${amount} ${session.currency?.toUpperCase()}.
`, }), }); // Let Stripe retry if Koltrix couldn't take it; the key prevents duplicates. if (!res.ok && res.status >= 500) return new Response("retry", { status: 500 }); } return new Response("ok"); } ``` ## GitHub notifications Email a code owner when a pull request is opened against `main`: **Next.js route handler** ```ts export async function POST(req: Request) { // Verify X-Hub-Signature-256 here before trusting the payload. const { action, pull_request, repository } = await req.json(); if (action !== "opened" || pull_request.base.ref !== "main") { return new Response("ignored"); } await fetch("https://api.koltrix.com/api/v2/emails", { method: "POST", headers: { Authorization: `Bearer ${process.env.KOLTRIX_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": `pr-${repository.id}-${pull_request.number}-opened`, }, body: JSON.stringify({ from: "ci@acme.com", to: ["codeowner@acme.com"], subject: `[${repository.full_name}] ${pull_request.title}`, body_html: `Review #${pull_request.number}
`, }), }); return new Response("ok"); } ``` ## Slack alerts on bounces Forward `message.bounced` events to a Slack incoming webhook: **Next.js route handler** ```ts export async function POST(req: Request) { const raw = await req.text(); const expected = "sha256=" + crypto.createHmac("sha256", process.env.KOLTRIX_WEBHOOK_SECRET!).update(raw).digest("hex"); const given = req.headers.get("x-koltrix-signature") ?? ""; if ( given.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected)) ) { return new Response("bad signature", { status: 401 }); } const event = JSON.parse(raw); if (event.event === "message.bounced") { await fetch(process.env.SLACK_WEBHOOK_URL!, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: `:warning: "${event.subject}" bounced (${event.message_id}): ${event.error}`, }), }); } return new Response("ok"); } ``` One endpoint can fan out to several places: branch on `event.event`. The payload of each event is in [Webhooks](https://docs.koltrix.com/webhooks.md#payloads). ## Zapier, Make and n8n There is no Koltrix app in these marketplaces yet, but their generic HTTP steps work well: - **Send an email from a workflow:** add an HTTP request step (Zapier: *Webhooks by Zapier → Custom Request*) that `POST`s JSON to `https://api.koltrix.com/api/v2/emails` with the header `Authorization: Bearer kx_…`. Remember that `to` is an array. - **Start a workflow from a Koltrix event:** add the workflow's webhook URL as a Koltrix [webhook](https://docs.koltrix.com/webhooks.md). Its events arrive as JSON. Use a key with only the `send` scope for these tools. ## AI assistants To work with your inbox from Claude, ChatGPT, Cursor or another AI assistant, connect Koltrix's MCP server. See [AI assistants (MCP)](https://docs.koltrix.com/mcp.md). ## Incoming mail Mail to your domain arrives in your Koltrix team inbox. There is no webhook for incoming mail and no API to read it yet. To pass a mailbox's mail on to another system, set up **Settings → Forwarding**, or let an AI assistant read it through the [MCP connection](https://docs.koltrix.com/mcp.md). ## Languages There are no official SDKs. The API is a handful of JSON endpoints, and each language's standard HTTP client is enough. | Language | REST API | SMTP relay | | --- | --- | --- | | Node.js | `fetch` (Node 18+) | `nodemailer` | | Python | `requests` or `httpx` | `smtplib` | | Go | `net/http` | `net/smtp` (see the [relay page](https://docs.koltrix.com/smtp-relay.md#go-netsmtp) for authentication) | | PHP | cURL or Guzzle | PHPMailer | | Ruby | `Net::HTTP` | the `mail` gem | Code for each is in [Sending email](https://docs.koltrix.com/sending-email.md#code-samples) and [SMTP relay](https://docs.koltrix.com/smtp-relay.md#examples). --- # Changelog Source: https://docs.koltrix.com/changelog Changes that affect developers: the REST API, the SMTP relay, webhooks, domain setup and these docs. Newest first. For new features in the Koltrix app, see the [product changelog](https://koltrix.com/changelog). `/api/v2` is the stable API. Changes to it are additive: new fields and endpoints may appear, but existing fields, status codes and behaviour don't change without a new version and notice. ## 2026-10-04 **Interactive cards in AI assistants.** In an app that supports the MCP Apps extension, `list_mailboxes`, `search_mail`, `list_threads`, `get_inbox_summary`, `get_thread` and `create_draft` now also show a card: a mailbox picker, a conversation list, a plain-text conversation view and a draft review card with **Send**, **Edit in Koltrix** and **Discard**. The server offers cards only to an app that declares MCP Apps support (`io.modelcontextprotocol/ui`) when it connects, and serves the pages as `ui://koltrix/*` resources; other apps get exactly the text results they had. For an app that shows cards, sending moves to the Send button: `send_draft` is not offered, and the card calls a new app-only tool, `send_draft_from_card`, with a one-time token (30 minutes, single use, bound to the draft, the connection and the text shown) delivered only to the card. Every send check and limit is unchanged. Cards load nothing from the internet and show mail content as plain text. See [Interactive cards](https://docs.koltrix.com/mcp.md#interactive-cards). ## 2026-10-03 **AI assistants (MCP).** Connect Claude, ChatGPT, Cursor and other MCP apps to your Koltrix inbox at `https://mcp.koltrix.com/mcp`. Assistants can search, read, organise and draft; they can't forward or permanently delete mail. Sign-in is OAuth, with per-connection permissions, revocation and an admin switch. See [AI assistants](https://docs.koltrix.com/mcp.md). **AI assistants can send, if you allow it.** A new `send_draft` tool sends a draft you already have, but only when a workspace owner or admin has turned on **Allow assistants to send email after you confirm** (off by default), you have opted in with the new `mail.send` permission when connecting, and the assistant has shown you the message and got your yes. The call must carry `confirmed: true` and a `confirmation` of the form `a@x.com, b@y.com | Subject` matching the draft's recipients and subject. Limits: 10 recipients per message, 20 sends a day per connection and 50 a day per workspace. Connections made before this can't send until they are reconnected with the box ticked. Koltrix's own AI in the app is unchanged: it never sends without a click. See [Sending email from an assistant](https://docs.koltrix.com/mcp.md#sending-email-from-an-assistant). **Docs.** Every page was checked against the API's code and corrected. New pages: [Errors](https://docs.koltrix.com/errors.md), [Deliverability and suppressions](https://docs.koltrix.com/deliverability.md) and this changelog. Known gaps are now marked "Current limitation" where they apply. ## 2026-10-02 **Five DNS records for new domains.** A new domain now asks for an ownership record (`_koltrix`), MX, SPF (`include:_spf.koltrix.com`), its own DKIM key (`kx1._domainkey`) and DMARC. If your domain already has an SPF record, Koltrix shows a merged one. Domains verified before this keep working unchanged and show the new records as a recommended upgrade; once their `kx1._domainkey` record checks out, their mail is signed with their own key automatically. See [Custom domains and DNS](https://docs.koltrix.com/domains.md). **Domain claims.** A domain can be pending in more than one workspace, so nobody can block its real owner by adding it first. The first workspace whose own ownership record appears in DNS verifies it. ## 2026-10-01 **Developers hub.** API keys, SMTP settings, webhooks and delivery logs are gathered under **Developers** in the app, with a copy-and-run quickstart. ## 2026-09-29 **Send quotas on the API and SMTP relay.** Sends through `POST /api/v2/emails` and the SMTP relay count against your plan's API send allowance, per recipient. Over the allowance the API answers `429` with `"code": "quota_exceeded"` and `resets_at`, and the relay answers a temporary `452`, so mail servers retry rather than drop the message. See [Limits and quotas](https://docs.koltrix.com/limits.md). ## 2026-09-01 **Per-key rate limit.** `/api/v2` allows 60 requests per minute per API key and answers `429` with `Retry-After` above that. ## 2026-08-31 **SMTP relay limits.** At most 1,000 recipients per message (`452 4.5.3` above that, so the rest can be sent in another message), and a message over 25 MB is refused with a permanent `552 5.3.4` instead of a retryable error. ## 2026-08-30 **Idempotency without duplicates.** The `Idempotency-Key` is claimed before a message is queued, so a retry that races the original can no longer send a second copy. A retry during the original answers `409`; if the idempotency store is unavailable the API answers `503` and sends nothing. **Bounces that come back later.** A permanent bounce report that arrives after the message was sent now marks the message `bounced` and adds the address to the suppression list. **Unsubscribes suppress.** Unsubscribing, from a link or through the API, adds the address to the workspace's suppression list, so no list, sequence or transactional send mails it again. ## 2026-07-30 **Display names in From.** `"from": "Acme