# Xipler API Documentation

**Base URL:** `https://app.xipler.com`
**API version:** `v1`
**Support:** info@elphinstone.nl

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.

---

## Table of contents

1. [Base URL & versioning](#base-url--versioning)
2. [Authentication](#authentication)
3. [Multi-tenant model](#multi-tenant-model)
4. [Sending email](#sending-email)
5. [SMTP relay](#smtp-relay)
6. [Domains](#domains)
7. [Templates](#templates)
8. [Contacts & tags](#contacts--tags)
9. [Webhooks](#webhooks)
10. [Suppressions](#suppressions)
11. [Stats & analytics](#stats--analytics)
12. [Inbound email](#inbound-email)
13. [Errors & rate limits](#errors--rate-limits)
14. [Integration patterns](#integration-patterns)
15. [Full endpoint reference](#full-endpoint-reference)

---

## Base URL & versioning

All REST endpoints live under `https://app.xipler.com/api/v1/*`.

SMTP relay: `smtp.xipler.com:465` (implicit TLS, recommended). Port `587` (STARTTLS) is available but limited.

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 a version bump.

---

## Authentication

Every request needs a Bearer token in the `Authorization` header:

```
Authorization: Bearer xp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

### API key types

- `xp_live_` — production. Real emails delivered via SES.
- `xp_test_` — test mode. Emails are 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: `send` and/or `read`. Sending endpoints require `send`; read endpoints accept either. Missing permission returns:

```json
{ "error": "API key does not have 'send' permission" }
```

### Key security

Keys are stored as SHA-256 hashes. Plaintext is shown once at creation. Revocation propagates within 5 minutes (in-memory cache TTL).

---

## Multi-tenant model

```
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.

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.

---

## 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. 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.**
- `track_opens` / `track_clicks` — override project-level tracking defaults per call.

### Response

```json
{
  "id": "f8a1c4e2-...",
  "status": "queued",
  "from": "noreply@yourdomain.com",
  "to": "user@example.com"
}
```

### Other email endpoints

```
GET    /api/v1/emails              List (paginated, ?limit=, ?offset=, ?status=)
GET    /api/v1/emails/:id          Details + tracked events
DELETE /api/v1/emails/:id          Cancel queued/scheduled email
POST   /api/v1/emails/batch        Send up to 100 emails in one request
```

---

## SMTP relay

For tools that only support SMTP (Supabase Auth, WordPress, Auth0, GoHighLevel), Xipler exposes an SMTP relay backed by the same pipeline as the REST API. All tracking, suppressions, and webhooks fire identically.

### Connection config

| Field | Value |
|---|---|
| Host | `smtp.xipler.com` |
| Port | `465` (implicit TLS, recommended) |
| Encryption | SSL/TLS |
| Username | `apikey` |
| Password | your `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

```
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 sending from a domain, it must be registered and its DNS verified. Xipler auto-generates the required DKIM, MAIL FROM, and SPF records.

### 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.

### Other domain endpoints

```
GET    /api/v1/domains              List all
GET    /api/v1/domains/:id          One domain + DNS records
PATCH  /api/v1/domains/:id/verify   Force a verification re-check
DELETE /api/v1/domains/:id          Remove (also unregisters in SES)
```

`domain.verified` and `domain.created`/`domain.deleted` webhook events fire automatically.

---

## Templates

Templates are HTML fragments with `{{variable}}` placeholders.

### 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              List
GET    /api/v1/templates/:slug        One
DELETE /api/v1/templates/:slug        Remove
```

---

## Contacts & tags

Contacts are people you can reach — typically your customers or their end-users. Tags are labels for segmentation and automations. Lists are explicit mailing-list memberships.

### POST /api/v1/contacts (upsert)

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 `trial` → `paid` in one call.
- `properties` — flexible key/value, string/number/boolean/null only.

### Common scenarios

**Trial signup → `trial` tag:**

```javascript
await 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
await 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 endpoints

```
GET /api/v1/contacts                       List (paginated, ?status=)
GET /api/v1/contacts/:id                   One contact + tags + lists
GET /api/v1/contacts/by-email/:email       Lookup by email (URL-encoded)
```

### Other contact endpoints

```
PATCH  /api/v1/contacts/:id                Update name, status, properties
DELETE /api/v1/contacts/:id                GDPR delete
POST   /api/v1/contacts/:id/tags           Add a single tag (by id or name)
DELETE /api/v1/contacts/:id/tags/:tagId    Remove tag (requires tag id)
GET    /api/v1/contacts/:id/events         Contact event history
```

### Tags

```
GET    /api/v1/tags          List
POST   /api/v1/tags          Create (with color, description)
DELETE /api/v1/tags/:id      Delete (also unlinks from all contacts)
```

Tags are usually auto-created via `POST /contacts`. Explicit creation is only needed when setting a custom color or description.

### Lists

```
GET /api/v1/lists            List mailing lists (managed in dashboard)
```

---

## Webhooks

Configure 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:

```
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 the parsed JSON.

### Retry behavior

On 5xx or timeout, Xipler retries with exponential backoff: 1s, 2s, 4s (up to 3 attempts). Return `2xx` quickly; do 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 in 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                       List
GET    /api/v1/inbound/emails/:id                   One inbound email
POST   /api/v1/inbound/emails/:id/forward           Forward to another address
POST   /api/v1/inbound/emails/:id/reply             Reply from the inbound address
GET    /api/v1/inbound/addresses                    List routing rules
POST   /api/v1/inbound/addresses                    Create routing rule
DELETE /api/v1/inbound/addresses/:id                Remove rule
```

---

## Errors & rate limits

### Error shape

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

### Status codes

| Code | Meaning |
|---|---|
| 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` (max 255 chars) on `POST /emails`. Subsequent requests with the same key + project return the original record without re-sending.

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

### Pattern A: SaaS sending on behalf of customers

The most common pattern. Your platform sends emails from each customer's domain.

**Setup:**
- One Xipler project for the whole SaaS
- One API key in your environment
- N domains, one per customer, added via API at signup
- Project setting `allowApiFromOverride: true`

**Customer signup flow:**

```javascript
// 1. Customer enters their domain
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.
//    Save domain.id linked to your customer record.

// 3. Trigger verification when ready (or wait for the domain.verified webhook):
await fetch(`https://app.xipler.com/api/v1/domains/${domain.id}/verify`, {
  method: "PATCH",
  headers: { Authorization: `Bearer ${KEY}` },
});

// 4. Once verified, send from the customer's 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}`,
  }),
});
```

The `from` field on each webhook event lets you attribute bounces back to the right customer.

### Pattern B: 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.

### Pattern C: Separating transactional and marketing

Two projects: `Transactional` and `Marketing`. Different keys, different suppression lists. Marketing bounces don't block future transactional mail.

---

## 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
```

---

Last updated: 2026-05-18. Questions: info@elphinstone.nl.
