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_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

API 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 + analytics

Each 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

POST/api/v1/emails

Full 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.
  • html or text — 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 from data.
  • attachments — max 10. Use content (base64) for local files or path (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

GET/api/v1/emails

Returns up to 100 emails. Query params:

  • limit — max 100, default 50
  • offset — pagination offset
  • status — filter by status

Email details + events

GET/api/v1/emails/:id

Returns the email row + all tracked events (sent, delivered, opened, clicked, bounced, complained).

Cancel a scheduled email

DELETE/api/v1/emails/:id

Cancels a queued or scheduled email. Already-sent emails cannot be unsent.

Batch send

POST/api/v1/emails/batch

Send 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

Hostsmtp.xipler.com
Port465 (implicit TLS, recommended)
EncryptionSSL/TLS
Usernameapikey
Passwordyour xp_live_… API key

Example: Nodemailer

javascript
import 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_xxxxx

Rules

  • 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

POST/api/v1/domains
json
{ "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

PATCH/api/v1/domains/:id/verify

Forces 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

GET/api/v1/domains
GET/api/v1/domains/:id

Returns all domains (or one) with their DNS records.

Remove a domain

DELETE/api/v1/domains/:id

Removes 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

POST/api/v1/templates
json
{ "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

GET/api/v1/templates
GET/api/v1/templates/:slug
DELETE/api/v1/templates/:slug

Contacts & 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

POST/api/v1/contacts

Idempotent 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 flipping trialpaid in one call.
  • properties — flexible key/value, string/number/boolean/null only.

Common scenarios

Trial signup → tag trial:

javascript
fetch("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):

javascript
fetch("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

GET/api/v1/contacts
GET/api/v1/contacts/:id
GET/api/v1/contacts/by-email/:email

Use by-email for the common case of finding a contact you only have the email for. Returns 404 if not found.

Update a contact

PATCH/api/v1/contacts/:id

Update first/last name, status, properties. For tag changes prefer the upsert endpoint.

Delete a contact (GDPR)

DELETE/api/v1/contacts/:id

Permanent. Fires contact.deleted webhook.

Add or remove a single tag

POST/api/v1/contacts/:id/tags
DELETE/api/v1/contacts/:id/tags/:tagId

Useful when you don't want a full upsert. POST accepts tagId or tagName in the body. DELETE requires the tag id.

Contact event history

GET/api/v1/contacts/:id/events

Returns custom events tracked for the contact (page views, button clicks, anything you ingest).

Tag management

GET/api/v1/tags
POST/api/v1/tags
DELETE/api/v1/tags/:id

Tags are usually auto-created via POST /contacts. Explicit creation is only needed when you want to set a color or description.

Lists

GET/api/v1/lists

Read-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

eventtrigger
sentEmail accepted by SES
deliveredDelivered to recipient
openedTracking pixel loaded
clickedTracked link clicked
bouncedSoft or hard bounce
complainedSpam complaint
failedSend failed
email.receivedInbound email arrived
contact.createdContact created
contact.updatedContact updated
contact.deletedContact deleted
domain.createdDomain registered
domain.verifiedDomain DKIM verified
domain.deletedDomain 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:45Z

Verify in Node.js:

javascript
import 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

GET/api/v1/stats?days=30

Returns 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.received webhook event

Inbound endpoints

GET/api/v1/inbound/emails
GET/api/v1/inbound/emails/:id
POST/api/v1/inbound/emails/:id/forward
POST/api/v1/inbound/emails/:id/reply
GET/api/v1/inbound/addresses
POST/api/v1/inbound/addresses
DELETE/api/v1/inbound/addresses/:id

Errors & rate limits

Error shape

json
{ "error": "Human-readable description of what went wrong" }

Status codes

200 / 201Success
400Validation error, invalid input, or unverified domain
401Missing or invalid API key
403API key lacks the required permission
404Resource not found (within your project)
409Conflict — duplicate domain, duplicate template slug
422Email is on the suppression list
429Rate limit exceeded — see Retry-After header
500Server error
502Upstream 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: true so the from field 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

POST/api/v1/emails
POST/api/v1/emails/batch
GET/api/v1/emails
GET/api/v1/emails/:id
DELETE/api/v1/emails/:id

Templates

POST/api/v1/templates
GET/api/v1/templates
GET/api/v1/templates/:slug
DELETE/api/v1/templates/:slug

Domains

POST/api/v1/domains
GET/api/v1/domains
GET/api/v1/domains/:id
PATCH/api/v1/domains/:id/verify
DELETE/api/v1/domains/:id

Contacts

POST/api/v1/contacts
GET/api/v1/contacts
GET/api/v1/contacts/:id
GET/api/v1/contacts/by-email/:email
PATCH/api/v1/contacts/:id
DELETE/api/v1/contacts/:id
POST/api/v1/contacts/:id/tags
DELETE/api/v1/contacts/:id/tags/:tagId
GET/api/v1/contacts/:id/events

Tags & lists

GET/api/v1/tags
POST/api/v1/tags
DELETE/api/v1/tags/:id
GET/api/v1/lists

Inbound

GET/api/v1/inbound/emails
GET/api/v1/inbound/emails/:id
POST/api/v1/inbound/emails/:id/forward
POST/api/v1/inbound/emails/:id/reply
GET/api/v1/inbound/addresses
POST/api/v1/inbound/addresses
DELETE/api/v1/inbound/addresses/:id

Other

GET/api/v1/stats
GET/api/v1/health