Xipler API Documentation
Send transactional email at scale. REST + SMTP, multi-domain, with tracking, suppressions, contacts and webhooks.
Base URL: https://app.xipler.com — API version v1
Introduction
Xipler is a transactional email platform. You can send mail through a REST API or through an SMTP relay that points at the same pipeline. Both produce identical tracking, suppression handling, webhook events, and analytics — so you can mix-and-match per integration without losing visibility.
Under the hood, mail is delivered through AWS SES with per-domain DKIM signing. Xipler adds the multi-tenant control plane: organizations, projects, API keys, domain verification, templates, contact management, webhooks, suppression lists, idempotency, and rate limiting.
This documentation is a complete API reference. All endpoints, schemas, and patterns are documented here.
Base URL & versioning
All REST endpoints live under:
https://app.xipler.com/api/v1/*SMTP relay is reachable at:
smtp.xipler.com:465 (implicit TLS — recommended)
smtp.xipler.com:587 (STARTTLS — limited availability)The API is currently v1. Breaking changes ship as a new version path; v1 remains compatible. Additive changes (new optional fields, new endpoints) ship without version bumps.
Authentication
Every request needs a Bearer token in the Authorization header:
Authorization: Bearer xp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxAPI key types
Two prefixes signal two different runtime behaviors:
xp_live_— production. Real emails delivered via SES.xp_test_— test mode. Email is fully validated and logged in the dashboard but never handed to SES. Domain verification is skipped, so you can wire integrations before any DNS is configured.
Permissions
Each key has a set of permissions (currently send and read). Sending endpoints require send; read endpoints accept either. A missing permission returns:
{ "error": "API key does not have 'send' permission" }Key security
Keys are stored as SHA-256 hashes. The plaintext is shown once at creation and never displayed again. Lost a key — generate a new one and delete the old. Revocation propagates within 5 minutes (the in-memory cache TTL).
Multi-tenant model
The hierarchy:
Organization
└─ Projects (1+)
├─ API keys
├─ Domains (verified senders)
├─ Templates
├─ Webhooks
├─ Suppression list
├─ Contacts + tags + lists
└─ Emails + analyticsEach project is fully isolated. API keys in project A cannot read or write resources in project B, even within the same organization. The suppression list, templates, webhooks, statistics — all project-scoped.
For SaaS platforms sending on behalf of customers, use one project with many verified domains. You get one API key, one dashboard, one set of templates, and centralised suppression handling. See Integration patterns for details.
Sending email
/api/v1/emailsFull schema (most fields optional — only to, subject, and either html or text are required):
json{ "to": "user@example.com", "toName": "User", "from": "noreply@yourdomain.com", "fromName": "Your Company", "replyTo": "reply@yourdomain.com", "cc": ["cc@example.com"], "bcc": "bcc@example.com", "subject": "Subject (max 500 chars)", "html": "<p>Hello</p>", "text": "Hello (plain text fallback)", "headers": { "X-Custom-Header": "value" }, "template": "session-reminder", "data": { "name": "Janneke", "date": "16 May" }, "tags": ["transactional"], "attachments": [ { "filename": "invoice.pdf", "content": "<base64...>" }, { "filename": "logo.png", "path": "https://example.com/logo.png" } ], "scheduledAt": "2026-05-20T09:00:00Z", "idempotencyKey": "session-reminder-abc-123", "track_opens": true, "track_clicks": true }
Field reference
to— string or"Name <email>". Required.from— must match a verified domain in the project. If omitted, the project default sender is used.subject— max 500 chars. Required.htmlortext— at least one required. Plaintext is auto-generated from HTML if not provided.template— slug of a stored template.{{variable}}placeholders in the template are filled fromdata.attachments— max 10. Usecontent(base64) for local files orpath(HTTPS URL) for remote.scheduledAt— ISO 8601 datetime in the future. Email is queued, not sent immediately.idempotencyKey— string, max 255 chars. Replays with the same key return the original record without re-sending. Strongly recommended for user-facing mail.track_opens/track_clicks— override the project-level tracking defaults per call.
Response
json{ "id": "f8a1c4e2-...", "status": "queued", "from": "noreply@yourdomain.com", "to": "user@example.com" }
The id is the canonical email identifier. Use it to look up the email later or correlate with webhook events.
List emails
/api/v1/emailsReturns up to 100 emails. Query params:
limit— max 100, default 50offset— pagination offsetstatus— filter by status
Email details + events
/api/v1/emails/:idReturns the email row + all tracked events (sent, delivered, opened, clicked, bounced, complained).
Cancel a scheduled email
/api/v1/emails/:idCancels a queued or scheduled email. Already-sent emails cannot be unsent.
Batch send
/api/v1/emails/batchSend up to 100 emails in one request. Body is { "emails": [...] } where each item follows the single-email schema above. Returns per-email results.
SMTP relay
For tools that only support SMTP (Supabase Auth, WordPress, Auth0, GoHighLevel, etc.), Xipler exposes an SMTP relay backed by the same pipeline as the REST API. All tracking, suppressions, and webhooks fire identically.
Connection config
| Host | smtp.xipler.com |
| Port | 465 (implicit TLS, recommended) |
| Encryption | SSL/TLS |
| Username | apikey |
| Password | your xp_live_… API key |
Example: Nodemailer
javascriptimport nodemailer from "nodemailer"; const transport = nodemailer.createTransport({ host: "smtp.xipler.com", port: 465, secure: true, // implicit TLS, NOT STARTTLS auth: { user: "apikey", pass: process.env.XIPLER_API_KEY, }, }); await transport.sendMail({ from: '"Your Company" <noreply@yourdomain.com>', to: "user@example.com", subject: "Hello", html: "<p>Sent via SMTP</p>", });
Example: Supabase Auth
In the Supabase dashboard → Authentication → Email Templates → SMTP Settings:
Sender email: noreply@yourdomain.com
Host: smtp.xipler.com
Port: 465
Username: apikey
Password: xp_live_xxxxxRules
- SMTP is opt-in per project. Enable in Project Settings → SMTP Relay.
- The
From:header must match a verified domain in the authenticating project. - Suppressed recipients are rejected at RCPT TO with
550 5.1.1. - Hard project isolation — a key for project A cannot send from domains verified in project B.
Domains
Before you can send from a domain, it must be registered and its DNS verified. Xipler auto-generates the required records (DKIM, MAIL FROM, SPF) when you add the domain.
Register a domain
/api/v1/domainsjson{ "domain": "customer.com" }
Response:
json{ "domain": { "id": "dom_a1b2c3...", "domain": "customer.com", "status": "pending", "dkimTokens": ["abc...", "def...", "ghi..."], "mailFromDomain": "bounce.customer.com", "dnsRecords": [ { "type": "CNAME", "name": "abc._domainkey.customer.com", "value": "abc.dkim.amazonses.com" }, { "type": "CNAME", "name": "def._domainkey.customer.com", "value": "def.dkim.amazonses.com" }, { "type": "CNAME", "name": "ghi._domainkey.customer.com", "value": "ghi.dkim.amazonses.com" }, { "type": "MX", "name": "bounce.customer.com", "value": "feedback-smtp.eu-central-1.amazonses.com", "priority": 10 }, { "type": "TXT", "name": "bounce.customer.com", "value": "v=spf1 include:amazonses.com ~all" } ] } }
Display these records to the domain owner. They'll add them at their DNS provider.
Trigger verification check
/api/v1/domains/:id/verifyForces a live re-check with AWS SES + DNS. Returns updated status. The status reaches verified once DKIM is confirmed.
Alternative: subscribe to the domain.verified webhook event for passive monitoring.
List domains
/api/v1/domains/api/v1/domains/:idReturns all domains (or one) with their DNS records.
Remove a domain
/api/v1/domains/:idRemoves the domain from Xipler and AWS SES. Fires domain.deleted webhook.
Templates
Templates are HTML fragments with {{variable}} placeholders. Use them by passing template + data when sending.
Create a template
/api/v1/templatesjson{ "name": "Session Reminder", "slug": "session-reminder", "subject": "Reminder: your session with {{coachName}}", "html": "<p>Hi {{clientName}}, your session is on {{sessionDate}}.</p>", "variables": ["coachName", "clientName", "sessionDate"] }
Use in a send
json{ "to": "client@example.com", "template": "session-reminder", "data": { "coachName": "Janneke", "clientName": "Pieter", "sessionDate": "20 May 2026" } }
Other template endpoints
/api/v1/templates/api/v1/templates/:slug/api/v1/templates/:slugContacts & tags
Contacts are people you can reach — typically your customers or their end-users. Tags are labels you attach to contacts for segmentation and to trigger automations. Lists are explicit mailing-list memberships.
Create or enrich a contact
/api/v1/contactsIdempotent upsert by email. Tags are auto-created if they don't exist.
json{ "email": "user@example.com", "firstName": "Jane", "lastName": "Doe", "source": "trial-signup", "tags": ["trial"], "tagsRemove": ["lead"], "properties": { "plan": "trial", "trial_started_at": "2026-05-18T12:00:00Z" } }
tags— add (idempotent). Tag names not yet in the project are created automatically.tagsRemove— remove these tag names from this contact. Useful for flippingtrial→paidin one call.properties— flexible key/value, string/number/boolean/null only.
Common scenarios
Trial signup → tag trial:
javascriptfetch("https://app.xipler.com/api/v1/contacts", { method: "POST", headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ email: user.email, firstName: user.firstName, tags: ["trial"], properties: { plan: "trial", trial_started_at: new Date().toISOString() }, }), });
Convert trial → paid (one call):
javascriptfetch("https://app.xipler.com/api/v1/contacts", { method: "POST", headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ email: user.email, tags: ["paid"], tagsRemove: ["trial"], properties: { plan: "paid", converted_at: new Date().toISOString() }, }), });
Lookup contacts
/api/v1/contacts/api/v1/contacts/:id/api/v1/contacts/by-email/:emailUse by-email for the common case of finding a contact you only have the email for. Returns 404 if not found.
Update a contact
/api/v1/contacts/:idUpdate first/last name, status, properties. For tag changes prefer the upsert endpoint.
Delete a contact (GDPR)
/api/v1/contacts/:idPermanent. Fires contact.deleted webhook.
Add or remove a single tag
/api/v1/contacts/:id/tags/api/v1/contacts/:id/tags/:tagIdUseful when you don't want a full upsert. POST accepts tagId or tagName in the body. DELETE requires the tag id.
Contact event history
/api/v1/contacts/:id/eventsReturns custom events tracked for the contact (page views, button clicks, anything you ingest).
Lists
/api/v1/listsRead-only via the public API. Lists are managed in the dashboard.
Webhooks
Configure webhooks per project in the dashboard. Xipler POSTs JSON to your URL when subscribed events fire.
Event types
| event | trigger |
|---|---|
| sent | Email accepted by SES |
| delivered | Delivered to recipient |
| opened | Tracking pixel loaded |
| clicked | Tracked link clicked |
| bounced | Soft or hard bounce |
| complained | Spam complaint |
| failed | Send failed |
| email.received | Inbound email arrived |
| contact.created | Contact created |
| contact.updated | Contact updated |
| contact.deleted | Contact deleted |
| domain.created | Domain registered |
| domain.verified | Domain DKIM verified |
| domain.deleted | Domain removed |
Payload shape
json{ "event": "delivered", "timestamp": "2026-05-18T10:23:45Z", "data": { "emailId": "f8a1c4e2-...", "projectId": "9bbb241c-...", "to": "user@example.com", "from": "noreply@yourdomain.com", "messageId": "0100018f-...", "metadata": { "smtpResponse": "250 2.6.0 ...", "processingTimeMs": 423 } } }
Signature verification
Each request carries headers:
X-Xipler-Signature: sha256=<hex digest>
X-Xipler-Event: delivered
X-Xipler-Timestamp: 2026-05-18T10:23:45ZVerify in Node.js:
javascriptimport crypto from "crypto"; function verifyWebhook(rawBody, headerSig, secret) { const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex"); return crypto.timingSafeEqual( Buffer.from(headerSig), Buffer.from(expected) ); }
Compute against the raw bytes, not parsed JSON.
Retry behavior
On 5xx or timeout, Xipler retries with exponential backoff: 1s, 2s, 4s (up to 3 attempts). Return 2xx quickly; do any heavy work async on your side.
Suppressions
Recipients with permanent bounces or complaints are added to the suppression list automatically. Any future send to that address returns:
json{ "error": "Email '...' is suppressed (bounce). This address previously bounced or complained.", "suppression": { "reason": "bounce", "bounceType": "Permanent", "createdAt": "..." } }
Manage suppressions via the dashboard (Settings → Suppressions). The list is project-scoped.
Stats & analytics
/api/v1/stats?days=30Returns aggregate counts and daily breakdown:
json{ "totals": { "sent": 1234, "delivered": 1198, "opened": 643, "clicked": 87, "bounced": 12, "complained": 1 }, "daily": [ { "date": "2026-05-17", "sent": 42, "delivered": 41, ... } ] }
Default 7 days, max 365.
Inbound email
Receive email on a verified domain via MX routing. Each message is:
- MIME-parsed (from, to, subject, html, text, attachments)
- SPF/DKIM/DMARC/spam/virus verdicts attached
- Stored in S3 + accessible via API
- Available as the
email.receivedwebhook event
Inbound endpoints
/api/v1/inbound/emails/api/v1/inbound/emails/:id/api/v1/inbound/emails/:id/forward/api/v1/inbound/emails/:id/reply/api/v1/inbound/addresses/api/v1/inbound/addresses/api/v1/inbound/addresses/:idErrors & rate limits
Error shape
json{ "error": "Human-readable description of what went wrong" }
Status codes
| 200 / 201 | Success |
| 400 | Validation error, invalid input, or unverified domain |
| 401 | Missing or invalid API key |
| 403 | API key lacks the required permission |
| 404 | Resource not found (within your project) |
| 409 | Conflict — duplicate domain, duplicate template slug |
| 422 | Email is on the suppression list |
| 429 | Rate limit exceeded — see Retry-After header |
| 500 | Server error |
| 502 | Upstream error (typically SES) |
Idempotency
Pass an idempotencyKey string (max 255 chars) on POST /emails. Subsequent requests with the same key + project return the original record without re-sending. Strongly recommended for any retry-able send.
Rate limits
Standard per-API-key limits apply. Excess requests return 429 with a Retry-After header. Use exponential backoff on retries. SMTP relay has separate per-project limits configurable in project settings.
Integration patterns
SaaS sending on behalf of customers
Most common pattern. Your platform sends emails from the customer's domain so end-users see noreply@customer.com in their inbox, not your platform's domain.
Setup:
- One Xipler project for the whole SaaS
- One API key stored in your environment
- N domains — one per customer, added via API as customers sign up
- Project setting
allowApiFromOverride: trueso thefromfield can vary per call
Customer signup flow:
javascript// 1. Customer enters their domain in your UI const res = await fetch("https://app.xipler.com/api/v1/domains", { method: "POST", headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ domain: customerDomain }), }); const { domain } = await res.json(); // 2. Show domain.dnsRecords to the customer with copy buttons. // Save domain.id linked to your customer record. // 3. When customer claims DNS is set, trigger verification: await fetch(`https://app.xipler.com/api/v1/domains/${domain.id}/verify`, { method: "PATCH", headers: { Authorization: `Bearer ${KEY}` }, }); // OR subscribe to the 'domain.verified' webhook // 4. Once verified, send from their domain: await fetch("https://app.xipler.com/api/v1/emails", { method: "POST", headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ to: endUser.email, from: `noreply@${customer.domain}`, template: "...", data: { ... }, idempotencyKey: `notification-${eventId}`, }), });
Use the from field on each webhook event to attribute bounces back to the right customer.
Isolated project per customer
When each customer needs their own dashboard, suppression list, and stats (white-label use cases). Each customer = one Xipler project = one API key. More operational overhead — use only when isolation matters more than convenience.
Separating transactional and marketing
Use two projects: Transactional and Marketing. Different keys, different suppression lists. Marketing bounces don't block future transactional mail to the same address.
Full endpoint reference
Emails
/api/v1/emails/api/v1/emails/batch/api/v1/emails/api/v1/emails/:id/api/v1/emails/:idTemplates
/api/v1/templates/api/v1/templates/api/v1/templates/:slug/api/v1/templates/:slugDomains
/api/v1/domains/api/v1/domains/api/v1/domains/:id/api/v1/domains/:id/verify/api/v1/domains/:idContacts
/api/v1/contacts/api/v1/contacts/api/v1/contacts/:id/api/v1/contacts/by-email/:email/api/v1/contacts/:id/api/v1/contacts/:id/api/v1/contacts/:id/tags/api/v1/contacts/:id/tags/:tagId/api/v1/contacts/:id/eventsInbound
/api/v1/inbound/emails/api/v1/inbound/emails/:id/api/v1/inbound/emails/:id/forward/api/v1/inbound/emails/:id/reply/api/v1/inbound/addresses/api/v1/inbound/addresses/api/v1/inbound/addresses/:idOther
/api/v1/stats/api/v1/health