IXmailer
Log in Start free

Developer reference

IXmailer API documentation

Send transactional + bulk email via REST or SMTP. Below is the practical reference. For an interactive playground and full schema, see the live API reference.

Quickstart

Five minutes to your first send. Assumes you signed up at https://app.ixmailer.com and verified a sender domain.

  1. 1. Mint an API key at https://app.ixmailer.com/api-keys — copy the ixk_… token (shown once).
  2. 2. Verify your sender domain at https://app.ixmailer.com/domains — add the 4 DNS records, click “Re-check DNS”.
  3. 3. Send your first message:

curl

curl https://api.ixmailer.com/v1/messages \
  -H "Authorization: Bearer ixk_…" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "noreply@your-verified-domain.com",
    "to":   "you@example.com",
    "subject": "Hello from IXmailer",
    "text": "It works."
  }'

Response: 202 Accepted with { id, status: "QUEUED" }. The id is your handle for fetching status, opens, clicks, bounces.

Authentication

Three credential types. Pick the narrowest scope your integration needs.

Type Scope Transport Mint at
Workspace API key All verified domains in your workspace REST + SMTP (as password) /api-keys
Per-domain API key One domain only (*@…) REST + SMTP /domains/<id>?tab=smtp
Per-domain SMTP credential One domain only SMTP only /domains/<id>?tab=smtp

Headers (either works):

HTTP headers

Authorization: Bearer ixk_<32-char-suffix>
# or
X-IXmailer-API-Key: ixk_<32-char-suffix>

Per-domain keys return 403 KEY_DOMAIN_MISMATCH when From: doesn't end in the key's locked domain. Workspace keys can send from any verified domain.

Sending mail (REST)

POST /v1/messages — JSON body, returns 202 + id.

Node.js (fetch)

const res = await fetch("https://api.ixmailer.com/v1/messages", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.IXMAILER_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "noreply@acme.com",
    fromName: "Acme",
    to: "alice@example.com",
    subject: "Order confirmation",
    html: "<p>Thanks for your order.</p>",
    text: "Thanks for your order.",
    track: { opens: true, clicks: true },
  }),
});
const { id, status } = await res.json();

Python (requests)

import os, requests

r = requests.post(
    "https://api.ixmailer.com/v1/messages",
    headers={"Authorization": f"Bearer {os.environ['IXMAILER_API_KEY']}"},
    json={
        "from": "noreply@acme.com",
        "to": "alice@example.com",
        "subject": "Order confirmation",
        "html": "<p>Thanks for your order.</p>",
    },
)
r.raise_for_status()
print(r.json())  # {"id":"cmp...","status":"QUEUED"}

Idempotency

Pass Idempotency-Key: <your-uuid> to safely retry after a network blip — duplicate keys within 24h return the original response without re-sending.

Fetching status

GET /v1/messages/:id

curl https://api.ixmailer.com/v1/messages/<id> \
  -H "Authorization: Bearer ixk_…"

# {
#   "id": "cmp...",
#   "from": "noreply@acme.com",
#   "to": "alice@example.com",
#   "subject": "Order confirmation",
#   "htmlBody": "<p>Thanks...</p>",
#   "textBody": "Thanks...",
#   "status": "DELIVERED",
#   "attempts": 1,
#   "sendingIp": "91.233.11.43",
#   "recipientMx": "aspmx.l.google.com",
#   "smtpResponse": "250 2.0.0 OK",
#   "queuedAt": "...", "sentAt": "..."
# }

Sending mail (SMTP)

Point any SMTP client at our submission relay. Same auth as REST — your API key (or per-domain credential) is the password.

SMTP settings

Host:       smtp.ixmailer.org
Ports:      587 (STARTTLS, recommended)
            465 (TLS-on-connect)
Username:   <verified address — anything @ your verified domain>
Password:   <API key (ixk_…) OR per-domain SMTP password>
Encryption: required (STARTTLS or implicit TLS)

The relay rejects plaintext AUTH (538 Must issue a STARTTLS command first). Port 25 is for receiving DSN bounces; not used for customer submission.

Sender domains

Register a domain, add the DNS records, click Re-check DNS in the dashboard. DKIM is required — the others are recommended for stronger policy but verification doesn't gate on them. Mail authenticates end-to-end via DKIM alignment with your From: domain; SPF passes on our own envelope domain (bounce.ixmailer.org), so you don't have to set up SPF for it to work.

POST /v1/sender-domains

curl -X POST https://api.ixmailer.com/v1/sender-domains \
  -H "Authorization: Bearer ixk_…" \
  -H "Content-Type: application/json" \
  -d '{"domain":"acme.com"}'

# Response includes dnsRecords[]:
#   DKIM   ix._domainkey TXT    v=DKIM1;k=rsa;p=…         (required)
#   DMARC  _dmarc        TXT    v=DMARC1; p=quarantine; … (recommended)
#   SPF    @             TXT    v=spf1 include:spf.ixmailer.org -all  (optional)
#
# Anti-spoofing tip: even though SPF on your apex is optional FOR US,
# adding it protects your domain from being spoofed by other senders.

Once verified, set per-domain tracking + retention overrides under /domains/<id>?tab=settings. Per-domain values beat workspace defaults but lose to per-message overrides.

Contacts

A Contact is one recipient stored under your workspace. Contacts have an email, optional name + locale + timezone, free-form custom attributes (used as {{ contact.attributes.X }} merge tags), consent metadata for audit, and a status (SUBSCRIBED, UNSUBSCRIBED, CLEANED, or PENDING).

Create or upsert one

curl

curl -sS https://api.ixmailer.com/v1/contacts \
  -H "X-IXmailer-API-Key: $IXMAILER_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "email": "alice@example.com",
    "firstName": "Alice",
    "attributes": { "plan": "pro", "country": "AE" },
    "source": "signup-form"
  }'

Re-posting the same email is a no-error upsert that merges name + attribute fields. Status is preserved across re-imports — only the dedicated resubscribe path can move a CLEANED address back to SUBSCRIBED.

Bulk import (CSV or JSON)

curl

# CSV — auto-detects header row from the first line
curl -sS https://api.ixmailer.com/v1/contacts/bulk-import \
  -H "X-IXmailer-API-Key: $IXMAILER_API_KEY" \
  -H "content-type: application/json" \
  -d "{\"payload\": \"email,firstName\\nalice@example.com,Alice\\nbob@example.com,Bob\", \"listSlug\": \"newsletter\"}"

# JSON-array — use this when importing custom attributes
curl -sS https://api.ixmailer.com/v1/contacts/bulk-import \
  -H "X-IXmailer-API-Key: $IXMAILER_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "rows": [
      {"email":"alice@example.com","firstName":"Alice","attributes":{"plan":"pro"}}
    ],
    "listSlug": "newsletter"
  }'

Capped at 50,000 rows / ~5 MB per request. Re-importing an existing email merges attributes (existing keys are preserved unless overwritten).

Bulk action

curl

curl -sS https://api.ixmailer.com/v1/contacts/bulk-action \
  -H "X-IXmailer-API-Key: $IXMAILER_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "action": "add-to-list",
    "ids": ["cmcontact1", "cmcontact2"],
    "listSlug": "newsletter"
  }'

Actions: delete (soft), unsubscribe, add-to-list, remove-from-list. Capped at 500 ids per request. Cross-workspace ids are silently ignored, not errored.

Lists

A list is a named, manually-managed group of contacts. Slug is the public identifier; URL-friendly, unique per workspace. Campaigns send to one or more lists at a time, de-duplicated by contact id at the moment of materialization.

curl

# Create
curl -sS https://api.ixmailer.com/v1/contact-lists \
  -H "X-IXmailer-API-Key: $IXMAILER_API_KEY" \
  -H "content-type: application/json" \
  -d '{"name":"Newsletter Q2 2026","description":"Quarterly customer update"}'

# Update — including the public-signup toggle
curl -sS -X PATCH https://api.ixmailer.com/v1/contact-lists/newsletter \
  -H "X-IXmailer-API-Key: $IXMAILER_API_KEY" \
  -H "content-type: application/json" \
  -d '{"publicSubscriptionEnabled":true}'

# Members
curl -sS "https://api.ixmailer.com/v1/contact-lists/newsletter/members?limit=200" \
  -H "X-IXmailer-API-Key: $IXMAILER_API_KEY"

# Stats (counters by status — for the dashboard's stat cards)
curl -sS https://api.ixmailer.com/v1/contact-lists/newsletter/stats \
  -H "X-IXmailer-API-Key: $IXMAILER_API_KEY"

# Export every member as CSV
curl -sS https://api.ixmailer.com/v1/contact-lists/newsletter/export.csv \
  -H "X-IXmailer-API-Key: $IXMAILER_API_KEY" \
  -o newsletter-export.csv

Setting publicSubscriptionEnabled=true opens the list to unauthenticated POSTs via the public endpoints (below). Off by default so internal lists don't accidentally accept signups from the open internet.

Segments

A segment is a saved filter over your contacts, evaluated at send time. Use them to target campaigns like "engaged in the last 30 days" without manually maintaining a list.

curl

# Create — definition is { match: ALL | ANY, conditions: [...] }
curl -sS https://api.ixmailer.com/v1/segments \
  -H "X-IXmailer-API-Key: $IXMAILER_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "name": "Engaged last 30 days",
    "definition": {
      "match": "ALL",
      "conditions": [
        { "field": "status",        "op": "eq",          "value": "SUBSCRIBED" },
        { "field": "lastEngagedAt", "op": "within_days", "value": 30 }
      ]
    }
  }'

# Preview a definition before saving — returns count + 10-row sample
curl -sS https://api.ixmailer.com/v1/segments/preview \
  -H "X-IXmailer-API-Key: $IXMAILER_API_KEY" \
  -H "content-type: application/json" \
  -d '{"definition":{"match":"ALL","conditions":[
    {"field":"attribute.country","op":"eq","value":"AE"}
  ]}}'

Supported fields: status, list (membership in one or more list ids via in/not_in), createdAt, subscribedAt, lastEngagedAt, source, and attribute.<key> for any custom field. Operators per kind: enum eq/neq/in/not_in; date within_days/older_than_days/before/after/is_set; string eq/neq/contains/is_set. Max 20 conditions per segment.

Campaigns (bulk send)

A campaign is an outgoing email to many recipients at once. Each campaign carries an audience (lists + segments), an email body (either a saved template or inline HTML), and a sender. The full lifecycle is DRAFT → SCHEDULED → SENDING → SENT with PAUSED + CANCELED states for in-flight intervention.

curl

# Create a DRAFT (inline body)
curl -sS https://api.ixmailer.com/v1/campaigns \
  -H "X-IXmailer-API-Key: $IXMAILER_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "name": "May newsletter",
    "audienceListIds": ["cmlist123"],
    "audienceSegmentIds": ["cmseg456"],
    "fromAddress": "hello@yourdomain.com",
    "fromName": "Acme",
    "subject": "Hi {{ contact.firstName | default:\"there\" }}",
    "subjectVariants": ["Alternate subject B", "Alternate subject C"],
    "htmlBody": "<p>...</p><p><a href=\"{{ unsubscribe_url }}\">Unsubscribe</a></p>",
    "textBody": "..."
  }'

# Render preview — see resolved merge tags for a real contact (no send)
curl -sS https://api.ixmailer.com/v1/campaigns/may-newsletter/render-preview \
  -H "X-IXmailer-API-Key: $IXMAILER_API_KEY" \
  -H "content-type: application/json" \
  -d '{"contactEmail":"alice@example.com"}'

# Send test — up to 5 inbox previews, no campaign counters move
curl -sS https://api.ixmailer.com/v1/campaigns/may-newsletter/send-test \
  -H "X-IXmailer-API-Key: $IXMAILER_API_KEY" \
  -H "content-type: application/json" \
  -d '{"recipients":["you@yourdomain.com"]}'

# Schedule for later (or send now)
curl -sS https://api.ixmailer.com/v1/campaigns/may-newsletter/schedule \
  -H "X-IXmailer-API-Key: $IXMAILER_API_KEY" \
  -H "content-type: application/json" \
  -d '{"scheduledFor":"2026-06-01T12:00:00Z"}'

curl -sS -X POST https://api.ixmailer.com/v1/campaigns/may-newsletter/send \
  -H "X-IXmailer-API-Key: $IXMAILER_API_KEY"

Pre-send compliance scan

Every campaign hits GET /v1/campaigns/:slug/compliance-check before it can send. The scanner checks the From-domain is verified, the body contains an unsubscribe link, and the workspace's postal address is present (or referenced via {{ postal_address }}). It also runs warn-level checks for spam-trigger subjects, image-only bodies, and malformed merge tags. block-level findings prevent the send; warn findings are surfaced inline but don't gate it.

Built-in merge tags

{{ contact.firstName }}, {{ contact.lastName }}, {{ contact.email }}, {{ contact.attributes.<key> }} for custom fields, {{ company_name }} / {{ workspace.name }}, {{ company_website }}, {{ company_phone }}, {{ company_logo }} (URL — wrap in <img>), {{ postal_address }}, and the per-recipient {{ unsubscribe_url }}. Default fallback syntax: {{ contact.firstName | default:"there" }}.

Analytics

After a send, four analytics endpoints unlock — /stats (counters), /links (per-URL click breakdown, top 200), /timeline (hourly opens + clicks for 7d post-send), /by-isp (delivery + bounce + open rates per recipient domain), and /variant-stats (A/B subject performance, empty when not A/B-testing). All workspace-API-key authed.

Emergency controls

POST /campaigns/:slug/pause halts an in-flight send within the next 50-recipient checkpoint (workers nak queued jobs with a 30-second delay; resumable via /resume). /cancel is the hard stop — marks every still-queued Message as CANCELED so workers drop them on pickup. Already-handed-off SMTP sessions can't be recalled.

Public endpoints (no API key)

Rate-limited per source IP. Designed for direct hits from your website or marketing pages, so you never need to expose an API key client-side.

Public signup

curl

# 1. Flip publicSubscriptionEnabled on the list from your dashboard or:
curl -sS -X PATCH https://api.ixmailer.com/v1/contact-lists/newsletter \
  -H "X-IXmailer-API-Key: $IXMAILER_API_KEY" \
  -H "content-type: application/json" \
  -d '{"publicSubscriptionEnabled":true}'

# 2. Visitors POST directly — NO api key on this request
curl -sS https://api.ixmailer.com/v1/p/lists/newsletter/subscribe \
  -H "content-type: application/json" \
  -d '{"email":"visitor@example.com","firstName":"Visitor"}'

Consent IP + User-Agent captured on the resulting Contact row. Already-suppressed addresses get a silent subscribed:false response — attackers can't probe for who's opted out. A hosted form at https://app.ixmailer.com/p/forms/<slug> is also generated; share the link or paste an <iframe> into your site.

Subscriber preference center

When you flip the workspace's unsubscribeMode to PREFERENCE_CENTER, the {{ unsubscribe_url }} merge tag in campaign bodies points at a hosted page where recipients can keep some list subscriptions while opting out of others. The RFC 8058 List-Unsubscribe HTTP header always remains a one-click opt-out.

curl

# The token is the same HMAC-signed token in every campaign's unsubscribe URL.
GET  /v1/p/preferences?t=<token>
POST /v1/p/preferences?t=<token>             # body: { subscribedListSlugs: string[] }
POST /v1/p/preferences/unsubscribe-all?t=<token>

Webhooks

Subscribe to delivery events at https://app.ixmailer.com/webhooks. Each delivery is signed with HMAC-SHA256 in the X-IXmailer-Signature header.

message.queued

Accepted by the API.

message.sent

Handed to recipient MX with 2xx.

message.delivered

Confirmed delivery (250 OK).

message.deferred

Soft-bounced; will retry.

message.failed

Hard failure (bad address, content rejected).

message.bounced

Async DSN bounce received.

message.opened

Tracked open pixel hit.

message.clicked

Tracked link clicked.

message.complained

Recipient marked as spam.

message.unsubscribed

Recipient clicked the List-Unsubscribe link.

Signature verification

Node.js — HMAC verify

import crypto from "node:crypto";

function verifySignature(rawBody: string, signature: string, secret: string): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Error codes

Every non-2xx returns:

Error envelope

{
  "error": {
    "code": "STABLE_MACHINE_READABLE_CODE",
    "message": "Human-readable explanation.",
    "details": { … },           // optional, structured
    "requestId": "abc123…",
    "timestamp": "2026-05-14T19:00:00.000Z"
  }
}

Common codes:

NO_CREDENTIALS

No API key or session cookie on request.

INVALID_API_KEY

Key malformed, expired, or revoked.

IP_NOT_ALLOWED

Key's IP allowlist doesn't include caller IP.

KEY_DOMAIN_MISMATCH

Per-domain key but From: outside that domain.

FROM_DOMAIN_NOT_VERIFIED

Sender domain not registered + verified.

RATE_LIMIT_EXCEEDED

Workspace quota exhausted. retryAfter in body.

WORKSPACE_INACTIVE

Workspace suspended or pending review.

DUPLICATE_RECIPIENT

Same to: address sent within idempotency window.

ABUSE_GATE

Recent sends spiked bounce rate; sending paused.

INVALID_FROM

From address missing or malformed.

Rate limits

Quotas are per workspace, scaled by KYC tier. On hit, the API returns 429 RATE_LIMIT_EXCEEDED with retryAfter (seconds) in the body and Retry-After header.

Tier Per second Per minute Per day
Sandbox21005,000
Standard (KYC1)1050050,000
Pro (KYC2)502,500500,000
Enterprise20010,0005,000,000

Volume above the daily quota → deferred to the next day at midnight UTC. Request a tier upgrade from the Billing tab in the dashboard.

Building with AI? 🤖

We publish llms.txt and llms-full.txt — short + full machine-readable summaries an LLM can ingest in one fetch. Useful for code agents generating IXmailer integrations.