Quickstart
Five minutes to your first send. Assumes you signed up at https://app.ixmailer.com and verified a sender domain.
- 1. Mint an API key at https://app.ixmailer.com/api-keys — copy the
ixk_…token (shown once). - 2. Verify your sender domain at https://app.ixmailer.com/domains — add the 4 DNS records, click “Re-check DNS”.
- 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 |
|---|---|---|---|
| Sandbox | 2 | 100 | 5,000 |
| Standard (KYC1) | 10 | 500 | 50,000 |
| Pro (KYC2) | 50 | 2,500 | 500,000 |
| Enterprise | 200 | 10,000 | 5,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.