# IXmailer — Full developer reference > Generated for LLM ingestion. Single-file machine-readable dump of the > public API. For interactive exploration: https://ixmailer.com/docs/api > For the canonical contract: https://api.ixmailer.com/api/docs/openapi.json Last updated: 2026-05-14 --- ## What IXmailer is Bulk + transactional email SaaS operated by Innovatex Digital FZCO (Dubai). Production API at https://api.ixmailer.com, customer dashboard at https://app.ixmailer.com. MENA-focused infrastructure, sending from ixmailer.org reverse-DNS-verified IPs (91.233.11.43 + warm secondaries). Comparable to: Mailgun, SendGrid, Postmark, Resend. Wire-level API closely follows Mailgun conventions (intentional — eases migrations). --- ## Quickstart (curl) ```bash # 1. Mint an API key at https://app.ixmailer.com/api-keys (shown once) # 2. Verify your sender domain at /domains (add 4 DNS records) # 3. Send: 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 # { "id": "cmp...", "status": "QUEUED" } ``` --- ## Authentication Three credential types, all with prefix `ixk_` and all accepted on the same header. The API enforces scope server-side. | Type | Scope | Transport | Mint at | |------|-------|-----------|---------| | Workspace API key | All verified domains in workspace | REST + SMTP | `/api-keys` | | Per-domain API key | One domain (`*@…`) | REST + SMTP | `/domains/?tab=smtp` | | Per-domain SMTP credential | One domain | SMTP only | `/domains/?tab=smtp` | Headers (either works): ``` Authorization: Bearer ixk_<32-char-suffix> 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 in their workspace. Customer accounts log in to `app.ixmailer.com` with email + password (plus optional TOTP MFA). Operator accounts log in to `ops.ixmailer.com` with a SEPARATE password set under Settings → Operator console password. Operators may also be customers; separate password isolates the admin role (industry pattern: AWS root vs IAM, Stripe restricted keys). --- ## POST /v1/messages — send a message Request body: ```json { "from": "noreply@acme.com", // required; domain must be verified "fromName": "Acme", // optional; "Acme " "to": "alice@example.com", // required "replyTo": "support@acme.com", // optional "subject": "Order confirmation", // required "text": "Plain-text body", // optional but recommended "html": "

HTML body

", // optional "track": { // optional per-message override "opens": true, "clicks": true }, "campaignId": null, // optional, links to a Campaign "contactId": null // optional, links to a Contact } ``` Response: 202 Accepted ```json { "id": "cmp...", "status": "QUEUED" } ``` Headers: - `Authorization: Bearer ixk_…` OR `X-IXmailer-API-Key: ixk_…` (required) - `Idempotency-Key: ` (optional, 24h window) - `Content-Type: application/json` Idempotency: pass `Idempotency-Key` to safely retry after a network blip. Same key within 24h returns the original response, no double-send. ## GET /v1/messages/:id — fetch a sent message Response: ```json { "id": "cmp...", "from": "noreply@acme.com", "to": "alice@example.com", "subject": "Order confirmation", "htmlBody": "

...

", // null after retention purge "textBody": "...", // null after retention purge "status": "DELIVERED", // see lifecycle below "attempts": 1, "failureReason": null, "smtpResponse": "250 2.0.0 OK ...", "sendingIp": "91.233.11.43", "recipientMx": "aspmx.l.google.com", "queuedAt": "2026-05-14T17:11:54Z", "sentAt": "2026-05-14T17:11:55Z", "failedAt": null } ``` ## GET /v1/messages — recent messages Query params: - `limit` (default 50, max 200) - `cursor` for pagination --- ## Message lifecycle Status transitions: ``` QUEUED → SENDING → DELIVERED (happy path) ↘ → DEFERRED → SENDING (retried) ↘ → FAILED (hard reject, no retry) ↘ → BOUNCED (async DSN later) ``` Terminal states: DELIVERED, FAILED, BOUNCED. --- ## SMTP submission ``` Host: smtp.ixmailer.org Ports: 587 (STARTTLS, recommended) 465 (TLS-on-connect) User: Pass: ``` The relay rejects plaintext AUTH (`538 Must issue a STARTTLS command first`). Port 25 is for receiving DSN bounces; NOT for customer submission. Nodemailer example: ```js const transporter = nodemailer.createTransport({ host: "smtp.ixmailer.org", port: 587, secure: false, // STARTTLS upgrade requireTLS: true, auth: { user: "noreply@acme.com", pass: process.env.IXMAILER_SMTP_PASS }, }); ``` Postfix relayhost (in `/etc/postfix/main.cf`): ``` relayhost = [smtp.ixmailer.org]:587 smtp_sasl_auth_enable = yes smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd smtp_sasl_security_options = noanonymous smtp_use_tls = yes smtp_tls_security_level = encrypt ``` --- ## Sender domains ```bash # Register curl -X POST https://api.ixmailer.com/v1/sender-domains \ -H "Authorization: Bearer ixk_…" \ -d '{"domain":"mail.acme.com"}' ``` Response includes 4 DNS records to add at the customer's DNS provider: | Purpose | Host | Type | Value | |---------|------|------|-------| | SPF | `@` (or apex) | TXT | `v=spf1 include:spf.ixmailer.org -all` | | DKIM | `ix._domainkey` | TXT | `v=DKIM1;k=rsa;p=` | | DMARC | `_dmarc` | TXT | `v=DMARC1; p=quarantine; rua=mailto:dmarc@…` | | Return-Path | `bounces` | CNAME | `bounces.ixmailer.org` | All four must pass before the domain is VERIFIED and messages can send from it. Customer clicks "Re-check DNS" or a cron re-verifies every few minutes. Domain statuses: PENDING, VERIFYING, VERIFIED, FAILED, DISABLED. Per-domain settings (PATCH /v1/sender-domains/:id): - `trackOpens` true/false/null (null = inherit workspace default) - `trackClicks` true/false/null - `messageRetentionDays` 1-90 days --- ## Webhooks Subscribe at https://app.ixmailer.com/webhooks. Each delivery POSTs JSON to your URL with header `X-IXmailer-Signature: ` over the raw body using your endpoint's secret. Events (10): - `message.queued` — accepted by API - `message.sent` — handed to recipient MX (2xx) - `message.delivered` — confirmed 250 OK - `message.deferred` — soft-bounce, will retry - `message.failed` — hard reject, no retry - `message.bounced` — async DSN bounce received - `message.opened` — tracked pixel hit - `message.clicked` — tracked link clicked - `message.complained` — recipient marked as spam - `message.unsubscribed` — List-Unsubscribe link clicked Verify signature (Node.js): ```js import crypto from "node:crypto"; function verify(rawBody, signature, secret) { const expected = crypto.createHmac("sha256", secret) .update(rawBody).digest("hex"); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); } ``` Webhook payload shape: ```json { "event": "message.delivered", "messageId": "cmp...", "timestamp": "2026-05-14T17:11:55Z", "recipient": "alice@example.com", "data": { /* event-specific */ } } ``` Retry policy: exponential backoff for 24h on non-2xx response. --- ## Error envelope Every non-2xx response: ```json { "error": { "code": "STABLE_MACHINE_READABLE_CODE", "message": "Human-readable explanation.", "details": { /* optional structured data */ }, "requestId": "abc123...", "timestamp": "2026-05-14T19:00:00.000Z" } } ``` Common error codes: | Code | HTTP | Meaning | |------|------|---------| | NO_CREDENTIALS | 401 | No API key or session cookie on request | | NO_API_KEY | 401 | Missing X-IXmailer-API-Key header | | BAD_API_KEY | 401 | Malformed key (not `ixk_<43-char-suffix>`) | | INVALID_API_KEY | 401 | Key not found, expired, or revoked | | EXPIRED_API_KEY | 401 | Key past its expiresAt | | SENDER_DOMAIN_DELETED | 401 | Per-domain key whose domain was deleted | | IP_NOT_ALLOWED | 403 | Key's IP allowlist doesn't include caller IP | | KEY_DOMAIN_MISMATCH | 403 | Per-domain key but From: outside that domain | | OPS_PASSWORD_NOT_SET | 401 | Operator hasn't set their ops password yet | | NOT_OPERATOR | 403 | Non-operator hitting /v1/admin/* | | FROM_DOMAIN_NOT_VERIFIED | 400 | Sender domain not registered + verified | | INVALID_FROM | 400 | From address missing or malformed | | DOMAIN_TAKEN | 400 | Domain already registered in another workspace | | RESERVED_DOMAIN | 400 | Domain is reserved by IXmailer | | RATE_LIMIT_EXCEEDED | 429 | Workspace quota exhausted; retryAfter in body | | WORKSPACE_INACTIVE | 403 | Workspace suspended or pending review | | DUPLICATE_RECIPIENT | 409 | Same to: address sent within idempotency window | | ABUSE_GATE | 409 | Recent bounce-rate spike paused sending | | STATUS_UNCHANGED | 400 | Operator set status to current value | | STATUS_CHANGE_NEEDS_ACK | 422 | Operator status change needs acknowledge=true | All codes are stable — safe to switch on programmatically. --- ## Rate limits Per workspace, scaled by KYC tier: | 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 | On hit: `429 RATE_LIMIT_EXCEEDED` with `retryAfter` (seconds) in body + `Retry-After` header. Volume above daily quota → deferred to midnight UTC. Request a tier upgrade from the Billing tab in the dashboard. --- ## IP warmup + reputation Sending IPs are auto-warmed via a 30-day SendGrid/Sparkpost-style curve. A new IP starts at 50/day and grows to 200k/day by day 30. Auto-protection: every 5 minutes a cron checks per-IP bounce + complaint rates. Triggers (aggressive thresholds): - Bounce ≥ 2% (with ≥ 50 sends) OR any complaint → IP → COOLING - Bounce ≥ 5% OR ≥ 3 complaints → IP → DISABLED - 7 days clean AND no bounces/failures since cooled_at → COOLING → ACTIVE/WARMING Customer visibility: `sendingIp` in message responses + dashboard. The pool router round-robins across all WARMING+ACTIVE IPs. --- ## Suppressions Recipient-level opt-out list. Once an address is suppressed for a workspace, future sends to that address skip with status FAILED and reason `SUPPRESSED`. Triggers: - Hard bounce (550/553/etc.) → permanent suppression - Spam complaint → permanent suppression - List-Unsubscribe click → permanent suppression - Manual via dashboard → permanent - Manual via `POST /v1/suppressions` → permanent Sources tracked: `BOUNCE`, `SPAM_COMPLAINT`, `UNSUBSCRIBE`, `MANUAL`. Bulk import: `POST /v1/suppressions/import` with a CSV body, one address per line. --- ## Public (no-auth) endpoints Rate-limited per IP, no API key required. Designed for direct hits from a customer's website / marketing pages. ### Subscriber preference center - `GET /v1/p/preferences?t=` — read recipient's lists + workspace branding - `POST /v1/p/preferences?t=` — body `{ subscribedListSlugs: string[] }` - `POST /v1/p/preferences/unsubscribe-all?t=` — escape hatch Token is the HMAC-signed unsubscribe token embedded in every campaign email body (the `{{ unsubscribe_url }}` merge tag). Workspace must have `unsubscribeMode=PREFERENCE_CENTER` for the body link to point at the hosted page; otherwise the link lands on the legacy one-click confirmation. Hosted page lives at `https://app.ixmailer.com/p/preferences?t=…`. ### Public signup - `GET /v1/p/lists/:slug` — list metadata + workspace branding for the form - `POST /v1/p/lists/:slug/subscribe` — body `{ email, firstName?, lastName? }` Both endpoints 404 unless the list has `publicSubscriptionEnabled=true`. The customer toggles that from the dashboard's list detail page. Subscribe captures consent IP (from `X-Forwarded-For` or `req.ip`) and User-Agent on the Contact row for GDPR / CASL audit. Already- suppressed addresses get a silent `subscribed:false` response so attackers can't probe for whether an address opted out. Three integration paths: - Hosted form: `https://app.ixmailer.com/p/forms/:slug` (link or iframe) - Direct API: `POST /v1/p/lists/:slug/subscribe` from any frontend - Server-side: `POST /v1/contacts` with your API key + `source` field --- ## Compliance + privacy - PDPL (UAE Federal Decree-Law 45/2021) compliant - GDPR-compliant data residency (EU PoP available on Pro/Enterprise) - CASL-compliant unsubscribe (List-Unsubscribe RFC 8058 one-click) - Subscriber preference center: hosted page at app.ixmailer.com/p/preferences lets recipients keep some list subscriptions while dropping others. Opt-in per workspace via unsubscribeMode=PREFERENCE_CENTER (default is ONE_CLICK for backward compatibility). - Public signup forms: customers can flip publicSubscriptionEnabled on any list. Hosted form at app.ixmailer.com/p/forms/:slug or POST /v1/p/lists/:slug/subscribe direct from any frontend. Consent IP + User-Agent captured on every subscribe row. - Automatic suppression of bounces + complaints - Message retention: workspace default 30 days, per-domain override 1-90 - HMAC-SHA256 webhook signing - TLS-only SMTP submission --- ## Useful URLs (copyable) - API base: https://api.ixmailer.com - SMTP host: smtp.ixmailer.org - Dashboard: https://app.ixmailer.com - Operator (staff only): https://ops.ixmailer.com - Docs (this page = https://ixmailer.com/docs) - OpenAPI: https://api.ixmailer.com/api/docs/openapi.json - Interactive reference: https://ixmailer.com/docs/api - Status: https://status.ixmailer.com - Support: support@ixmailer.com --- ## SDK status No official SDK yet. Recommended HTTP clients: - JavaScript/TypeScript: native fetch, axios, ky, or undici - Python: requests, httpx - Go: net/http - PHP: Guzzle, native curl - Ruby: Faraday, net/http - Rust: reqwest The API is intentionally close to Mailgun's wire format — most Mailgun SDKs work with a base-URL change. See https://ixmailer.com/docs#mailgun-compat (when wired) for the explicit /v3//messages compatibility layer. --- End of llms-full.txt — last updated 2026-05-14.