KoltrixDocs

Sending email

Koltrix sends mail for your application in two ways, and both go through the same pipeline: the same suppression list, the same signing, the same message log and the same webhooks.

  1. The REST API: POST /api/v2/emails. The right choice for new code.
  2. The SMTP relay: for software that already speaks SMTP.

This page covers the REST API.

Before you send

  • An API key with the send scope. See Authentication.
  • 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, then click Add address on the domain and add the exact mailbox you will send from, for example [email protected].

POST /api/v2/emails

POST/api/v2/emails
cURL
curl https://api.koltrix.com/api/v2/emails \
  -H "Authorization: Bearer $KOLTRIX_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-receipt" \
  -d '{
    "from": "Acme <[email protected]>",
    "to": ["[email protected]"],
    "subject": "Your receipt for order 1042",
    "body_html": "<h1>Thanks!</h1><p>Your order is on its way.</p>",
    "body_text": "Thanks! Your order is on its way."
  }'

Request body

fromstringrequired
[email protected] or Acme <[email protected]>. The address must be an active address on a verified domain in this workspace.
tostring[]required
A JSON array, even for one recipient. At least one address.
ccstring[]
Additional recipients. See the note on recipients below.
bccstring[]
Additional recipients. See the note on recipients below.
subjectstringrequired
The subject line. Non-ASCII characters are encoded for you.
body_htmlstring
The HTML body. A fragment is fine; it is wrapped in a full HTML document.
body_textstring
The plain-text body. If you send only body_html, a text version is generated from it.

Send at least one of body_html and body_text.

Fields that are not in this table are ignored. In particular there is no reply_to, attachments, headers, template or scheduled send time on this endpoint today.

Headers

HeaderNotes
AuthorizationRequired. Bearer kx_…, a key with the send scope.
Content-Typeapplication/json.
Idempotency-KeyRecommended. Any string that identifies this logical send. See Retrying safely.

Response

202 Accepted once the message is queued:

{
  "id": "6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20",
  "status": "queued",
  "queued": true,
  "tracking_url": "/api/v2/messages/6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20",
  "recipient_count": 1
}
FieldNotes
idThe message id of the first recipient. Koltrix keeps one message record per recipient; list the others with GET /api/v2/messages?recipient=….
statusAlways queued in this response.
queuedAlways true in this response.
tracking_urlThe path to poll for this message's status.
recipient_countto + cc + bcc. Every recipient counts against your send quota.

Delivery happens a moment later in the background. Follow it by polling GET /api/v2/messages/:id or, better, with webhooks.

Errors

StatusBodyMeaning
400{"error": "invalid body"}The body is not valid JSON, or a field has the wrong type (for example to as a string instead of an array).
400{"error": "from, to, subject are required"}One of the three required fields is missing or empty.
400{"error": "from address is not a valid email"}from has no usable address in it.
401{"error": "invalid or revoked api key"}See Authentication.
403{"error": "missing permission: send"}The key has no send scope.
403{"error": "from address '…' is not registered for this account. …"}The From address isn't an active address in this workspace. The message ends by saying whether the domain is verified, so you know whether to add the address or verify the domain first.
409{"error": "a request with this Idempotency-Key is still in progress"}A retry arrived while the first request was still running.
429{"code": "quota_exceeded", …}Your plan's send quota is used up. See Limits and quotas.
429{"error": "rate limit exceeded — 60 requests per minute per API key"}Too many requests. Wait for Retry-After seconds.
503{"error": "idempotency store unavailable, please retry"}Nothing was sent. Retry with the same key.

Every error is listed with what to do about it in Errors.

Retrying safely

Networks fail after a request has been sent and before the answer arrives. Send an Idempotency-Key and a retry can never produce a second email.

  • Use one key per logical send, and reuse it on every retry: an order id plus the message type (order-1042-receipt) or a UUID you stored with the job. Never generate a new key inside the retry loop.
  • Keys are scoped to your workspace and remembered for 24 hours.
  • The key is claimed before anything is queued, so even two requests racing each other send one message.
SituationResponse
First request with this key202 with the queued message
Retry after the first one finished200 with the original body and the header Idempotent-Replayed: true
Retry while the first one is still running409: wait a moment and retry with the same key
Idempotency store unreachable503: nothing was sent, retry with the same key

A request that fails before anything is queued (a validation error, a quota refusal) releases the key, so you can fix the problem and retry with the same key straight away.

Koltrix does not compare request bodies. A second request with a used key gets the first request's response, even if its body is different.

A replay counts against the request rate limit but never against your send quota.

What happens after you send

  1. Suppressed recipients are skipped. An address on your suppression list is not mailed.
  2. The message is signed with your domain's DKIM key and handed to Koltrix's outbound mail servers.
  3. An open-tracking pixel is added to the HTML part.
  4. A copy appears in your team inbox, in the Sent folder, so teammates can see what your application sent.
  5. Replies come back to your From address, which is a real mailbox in your team inbox, threaded with the message. See Receiving replies.

Merge variables such as {{first_name}} are not substituted on this endpoint; the body is sent exactly as you wrote it. Build the final text in your application.

Message status

statusMeaning
queuedAccepted and waiting for the background worker. Usually under a second.
sentAccepted by Koltrix's outbound mail server for delivery to the recipient's provider.
bouncedRejected. Either the handoff failed, or the recipient's provider sent back a permanent bounce later. A permanent bounce adds the address to your suppression list.

Opens, clicks and replies don't change the status. They are counted in open_count, click_count and reply_count, with the time of the first one in opened_at, clicked_at and replied_at.

Checking a message

curl https://api.koltrix.com/api/v2/messages/6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20 \
  -H "Authorization: Bearer $KOLTRIX_KEY"
{
  "id": "6f1c2a9e-3b7d-4e41-9a52-0c8d1f3e7b20",
  "from": "[email protected]",
  "to": "[email protected]",
  "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": "[email protected]",
  "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
}
  • message_id is the Message-ID header of the sent mail, without the angle brackets.
  • error holds the reason when status is bounced.
  • engagement_score adds 1 per open, 5 per click and 10 per reply.
  • first_open_after_seconds is how long after sending the first open came. A few seconds usually means a mail provider fetched the images automatically, not that a person read it.

Requires the read or send scope.

For every open, click and reply with its time, IP address and user agent, call GET /api/v2/messages/:id/events. Opens that look like automatic image fetching are recorded with "prefetch": true and are not counted in open_count. Both endpoints are in the API reference.

Code samples

curl https://api.koltrix.com/api/v2/emails \
  -H "Authorization: Bearer $KOLTRIX_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-receipt" \
  -d '{
    "from": "[email protected]",
    "to": ["[email protected]"],
    "subject": "Your receipt",
    "body_html": "<p>Thanks for your order.</p>"
  }'

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 codeYour software can only send by SMTP
You want Idempotency-Key retriesYou can't change the code, only its settings
You want the message id back straight awayA 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 before you use it.