# Smailor API Reference — LLM Context File

> **Usage:** Drop this file into your project root (or paste it into your AI prompt/context) so any AI assistant — Claude, GPT, Cursor, Copilot, Gemini — can generate correct Smailor integration code. This file is the authoritative, machine-readable reference for the Smailor REST API.

---

## What Is Smailor?

Smailor is a **shared inbox and transactional email SaaS** built for indie founders and small technical teams. It handles:

- **Inbound email** — receives, routes, and threads customer emails as support tickets
- **Outbound transactional email** — send emails via REST API using your verified custom domain
- **Custom domain management** — add, verify (DNS), and monitor deliverability of your domains
- **Reply templates** — reusable email templates with variable interpolation (`{{customer_name}}`, etc.)
- **Team features** — group-based routing, staff assignments, Discord bot integration
- **Inbound webhooks** — get notified via HTTPS POST when emails arrive in your mailboxes

**Base URL:** `https://app.smailor.com`

---

## Authentication

All `/api/v1/*` endpoints require a Smailor API key. The Free plan does **not** have API access — Starter or higher is required.

Two equivalent authentication methods (use either, not both):

```http
Authorization: Bearer <your-api-key>
```
```http
X-API-Key: <your-api-key>
```

Obtain your key at: **Smailor Dashboard → Settings → API Keys**

Keys are sensitive — store them in environment variables, never in source code.

---

## Plans & Monthly Send Limits

| Plan | API Access | Monthly email sends |
|------|:----------:|:-------------------:|
| Free | No | — |
| Starter | Yes | 1,000 |
| Pro | Yes | 10,000 |
| Business | Yes | 50,000 |

The `remaining` field in send responses reflects how many sends are left in the current calendar month.

---

## Rate Limits

| Constraint | Value |
|------------|-------|
| Minimum gap between sends (per account) | 3 seconds |
| Max inbound webhook endpoints per account | 8 |
| Max mailbox `localPart` length | 64 characters |
| Max `subject` length | 998 characters |
| Max `text` body | 500,000 characters |
| Max `html` body | 2,000,000 characters |

When the per-account cooldown is active, the API returns `429` with a `Retry-After` header (seconds to wait).

---

## Endpoints

---

### POST /api/v1/send

Send a transactional email from one of your verified custom domain mailboxes.

**Key constraint:** `from` must be an active Smailor mailbox on a **verified custom domain** you own. Standard Smailor-hosted addresses (e.g. `@smailor.com`) are not available for API sends.

Each send creates a support ticket in your Smailor inbox (archived, closed by default) and logs the outbound reply for tracking.

#### Request

```http
POST https://app.smailor.com/api/v1/send
Authorization: Bearer <api-key>
Content-Type: application/json
```

#### Body

```json
{
  "to": "customer@example.com",
  "from": "support@yourdomain.com",
  "subject": "Order #1234 confirmed",
  "text": "Hi! Your order has been confirmed.",
  "html": "<p>Hi! Your order has been <strong>confirmed</strong>.</p>"
}
```

#### Body fields

| Field | Type | Required | Constraints |
|-------|------|:--------:|-------------|
| `to` | `string` (email) | Yes | Valid RFC 5321 address |
| `from` | `string` (email) | Yes | Active mailbox on your verified custom domain |
| `subject` | `string` | Yes | 1–998 characters |
| `text` | `string` | Conditional* | Max 500,000 characters |
| `html` | `string` | Conditional* | Max 2,000,000 characters. Sanitized server-side with DOMPurify. |

*At least one of `text` or `html` must be present and non-empty. Providing both is recommended (better deliverability).

#### Response `200 OK`

```json
{
  "ok": true,
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "ticketId": "b9a1c2d3-e4f5-6789-abcd-ef0123456789",
  "remaining": 847
}
```

| Field | Type | Description |
|-------|------|-------------|
| `ok` | `boolean` | Always `true` on success |
| `id` | `string` | UUID of the reply/send record |
| `ticketId` | `string` | UUID of the support ticket created (visible in dashboard) |
| `remaining` | `number` | Sends remaining this calendar month. `null` means unlimited. |

#### Error responses

| HTTP | `error` value | Meaning |
|------|--------------|---------|
| `401` | `unauthorized` | Missing or invalid API key |
| `403` | `forbidden` | Plan does not include API access (upgrade required) |
| `400` | `invalid_body` | Request body failed validation — check `details` |
| `400` | `invalid_from` | `from` is not an active address in your account |
| `400` | `custom_domain_required` | `from` must use a verified custom domain (not a Smailor-hosted domain) |
| `400` | `domain_not_verified` | Custom domain is linked but DNS verification is incomplete |
| `400` | `no_group` | Your account has no groups — create one in the dashboard first |
| `429` | `send_limit_reached` | Monthly send limit exhausted |
| `429` | `too_many_requests` | Per-account cooldown active — see `retryAfter` field (seconds) |
| `503` | `service_unavailable` | Queue temporarily unavailable |
| `500` | `internal_error` | Server error |

#### Code examples

**TypeScript / Node.js**

```typescript
const SMAILOR_API_KEY = process.env.SMAILOR_API_KEY!;
const SMAILOR_FROM = process.env.SMAILOR_FROM_ADDRESS!; // "support@yourdomain.com"

async function sendEmail(to: string, subject: string, html: string): Promise<void> {
  const res = await fetch("https://app.smailor.com/api/v1/send", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${SMAILOR_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      to,
      from: SMAILOR_FROM,
      subject,
      html,
      text: html.replace(/<[^>]*>/g, ""), // strip tags for plain text fallback
    }),
  });

  const data = await res.json();
  if (!res.ok) {
    throw new Error(`Smailor error [${data.error}]: ${data.message ?? ""}`);
  }
  // data.remaining tells you how many sends are left this month
  console.log(`Sent. Remaining this month: ${data.remaining}`);
}
```

**Python (httpx)**

```python
import httpx, os

SMAILOR_API_KEY = os.environ["SMAILOR_API_KEY"]
SMAILOR_FROM = os.environ["SMAILOR_FROM_ADDRESS"]  # "support@yourdomain.com"

def send_email(to: str, subject: str, html: str) -> dict:
    r = httpx.post(
        "https://app.smailor.com/api/v1/send",
        headers={"Authorization": f"Bearer {SMAILOR_API_KEY}"},
        json={
            "to": to,
            "from": SMAILOR_FROM,
            "subject": subject,
            "html": html,
        },
    )
    r.raise_for_status()
    return r.json()  # {"ok": True, "id": "...", "ticketId": "...", "remaining": 847}
```

**cURL**

```bash
curl -X POST https://app.smailor.com/api/v1/send \
  -H "Authorization: Bearer $SMAILOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "customer@example.com",
    "from": "support@yourdomain.com",
    "subject": "Hello",
    "text": "Hello from Smailor"
  }'
```

---

### GET /api/v1/domains

List all custom domains linked to your account, with their DNS verification status and deliverability reputation snapshot.

#### Request

```http
GET https://app.smailor.com/api/v1/domains
Authorization: Bearer <api-key>
```

No request body or query parameters.

#### Response `200 OK`

```json
{
  "domains": [
    {
      "id": "uuid",
      "domain": "yourdomain.com",
      "verified": true,
      "createdAt": "2025-01-15T12:00:00.000Z",
      "verifiedAt": "2025-01-15T14:32:00.000Z",
      "reputation": {
        "status": "healthy",
        "score": 98
      }
    },
    {
      "id": "uuid-2",
      "domain": "newdomain.com",
      "verified": false,
      "createdAt": "2025-05-20T09:00:00.000Z",
      "verifiedAt": null,
      "reputation": {
        "status": "unknown",
        "score": null
      }
    }
  ]
}
```

#### Domain object fields

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | UUID of the domain record |
| `domain` | `string` | Domain name (e.g. `"yourdomain.com"`) |
| `verified` | `boolean` | `true` if DNS records have been verified successfully |
| `createdAt` | `string` | ISO 8601 timestamp of when the domain was added |
| `verifiedAt` | `string \| null` | ISO 8601 timestamp of DNS verification, or `null` |
| `reputation.status` | `string` | `"healthy"` \| `"warning"` \| `"blocked"` \| `"unknown"` |
| `reputation.score` | `number \| null` | Deliverability score 0–100. `null` if no data yet. |

Domains are ordered: verified first, then by creation date (newest first).

#### Error responses

| HTTP | `error` value | Meaning |
|------|--------------|---------|
| `401` | `unauthorized` | Missing or invalid API key |

---

### POST /api/v1/mailboxes

Create a new email address (mailbox) on a verified custom domain in your account.

**Constraint:** The `domain` must already be verified via DNS in your Smailor account. The mailbox becomes immediately available for both inbound (receiving) and outbound (API send) use.

#### Request

```http
POST https://app.smailor.com/api/v1/mailboxes
Authorization: Bearer <api-key>
Content-Type: application/json
```

#### Body

```json
{
  "localPart": "support",
  "domain": "yourdomain.com",
  "displayName": "Acme Support"
}
```

#### Body fields

| Field | Type | Required | Constraints |
|-------|------|:--------:|-------------|
| `localPart` | `string` | Yes | 3–64 chars, lowercase alphanumeric + hyphens only. Pattern: `[a-z0-9][a-z0-9-]*[a-z0-9]` |
| `domain` | `string` | Yes | Must be a verified custom domain in your account |
| `displayName` | `string` | No | Max 80 chars. Appears as the sender name in email clients |

#### Response `201 Created`

```json
{
  "ok": true,
  "mailbox": {
    "id": "uuid",
    "address": "support@yourdomain.com",
    "localPart": "support",
    "domain": "yourdomain.com",
    "displayName": "Acme Support",
    "isPrimary": false
  },
  "reputation": {
    "status": "healthy",
    "score": 98
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `mailbox.id` | `string` | UUID of the new mailbox record |
| `mailbox.address` | `string` | The full email address created |
| `mailbox.isPrimary` | `boolean` | `true` if this is the first/primary address on the account |
| `reputation` | `object` | Domain reputation at creation time |

#### Error responses

| HTTP | `error` value | Meaning |
|------|--------------|---------|
| `401` | `unauthorized` | Missing or invalid API key |
| `403` | `forbidden` | Plan does not include API access |
| `400` | `invalid_body` | Validation failed — check field constraints |
| `400` | `domain_not_verified` | Domain is not verified in your account |
| `409` | `address_taken` | A mailbox with this address already exists |
| `409` | `domain_reputation_blocked` | Domain reputation degraded — new mailbox creation paused |

---

## Inbound Webhooks (Dashboard-Configured)

Inbound webhooks are configured in the **Smailor Dashboard → Settings → Webhooks** — they are not created via API. Up to 8 endpoints per account.

When a new email arrives at a monitored mailbox, Smailor fires a `POST` to your HTTPS endpoint.

**Webhook payload (reference)**

```json
{
  "event": "email.received",
  "ticketId": "uuid",
  "from": "sender@example.com",
  "to": "support@yourdomain.com",
  "subject": "I need help with my account",
  "text": "Plain text body of the email...",
  "receivedAt": "2025-01-15T12:00:00.000Z"
}
```

You can optionally configure a **shared secret** per endpoint (set in the dashboard). Verify the `X-Webhook-Secret` header value matches what you set.

Scope: webhooks can fire for all mailboxes, or be scoped to specific `addressId`s.

---

## CORS

All `/api/v1/*` endpoints support CORS preflight (`OPTIONS`). Browser-side API calls are supported.

Allowed headers: `Authorization`, `Content-Type`, `X-API-Key`  
Allowed methods: depends on endpoint (see each endpoint above)

---

## Common Integration Patterns

### Pattern 1: Transactional emails (order confirmations, password resets, notifications)

```typescript
// lib/email.ts
const BASE = "https://app.smailor.com";
const KEY = process.env.SMAILOR_API_KEY!;
const FROM = process.env.SMAILOR_FROM_ADDRESS!;

export async function sendTransactional(opts: {
  to: string;
  subject: string;
  html: string;
  text?: string;
}) {
  const res = await fetch(`${BASE}/api/v1/send`, {
    method: "POST",
    headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      to: opts.to,
      from: FROM,
      subject: opts.subject,
      html: opts.html,
      text: opts.text ?? opts.html.replace(/<[^>]*>/g, ""),
    }),
  });

  if (res.status === 429) {
    const body = await res.json();
    // Respect rate limit
    await new Promise((r) => setTimeout(r, (body.retryAfter ?? 3) * 1000));
    return sendTransactional(opts); // retry once
  }

  if (!res.ok) {
    const body = await res.json();
    throw Object.assign(new Error(body.message ?? body.error), { code: body.error });
  }

  return res.json() as Promise<{ ok: boolean; id: string; ticketId: string; remaining: number }>;
}
```

### Pattern 2: Verify domain is ready before sending

```typescript
async function assertDomainReady(domain: string): Promise<void> {
  const res = await fetch("https://app.smailor.com/api/v1/domains", {
    headers: { Authorization: `Bearer ${process.env.SMAILOR_API_KEY}` },
  });
  const { domains } = await res.json();
  const d = domains.find((d: any) => d.domain === domain);
  if (!d) throw new Error(`Domain ${domain} not found in Smailor account`);
  if (!d.verified) throw new Error(`Domain ${domain} is not DNS-verified yet`);
  if (d.reputation.status === "blocked") throw new Error(`Domain ${domain} reputation is blocked`);
}
```

### Pattern 3: Provision a mailbox when a customer signs up

```typescript
async function provisionCustomerMailbox(
  localPart: string,
  domain: string,
  displayName?: string
): Promise<{ address: string; id: string } | null> {
  const res = await fetch("https://app.smailor.com/api/v1/mailboxes", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SMAILOR_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ localPart, domain, displayName }),
  });

  const body = await res.json();

  if (res.status === 409 && body.error === "address_taken") {
    return null; // idempotent — already exists
  }

  if (!res.ok) {
    throw new Error(`Smailor mailbox creation failed: ${body.error} — ${body.message ?? ""}`);
  }

  return { address: body.mailbox.address, id: body.mailbox.id };
}
```

---

## Recommended Environment Variables

```env
# Required for API sends
SMAILOR_API_KEY=sk_live_...

# The "from" address used in your sends (must be a mailbox on a verified custom domain)
SMAILOR_FROM_ADDRESS=support@yourdomain.com

# Optional: override base URL (for testing against a self-hosted instance)
SMAILOR_BASE_URL=https://app.smailor.com
```

---

## Error Handling Checklist

When integrating, handle these cases explicitly:

- [ ] `401 unauthorized` — API key invalid or missing → surface a config error, do not retry
- [ ] `403 forbidden` — Plan too low → prompt user to upgrade, do not retry
- [ ] `400 custom_domain_required` — Wrong `from` domain → fix the `from` address in your config
- [ ] `400 domain_not_verified` — Wait for DNS propagation (up to 48h) → add a health-check before sending
- [ ] `429 send_limit_reached` — Monthly cap hit → queue for next month or notify user
- [ ] `429 too_many_requests` — Cooldown → respect `Retry-After` header, add delay before retry
- [ ] `503 service_unavailable` — Transient → retry with exponential backoff (3s, 9s, 27s)

---

## Glossary

| Term | Meaning |
|------|---------|
| **Ticket** | A support thread created when an email is received or sent via API |
| **Mailbox** | An email address configured in Smailor (e.g. `support@yourdomain.com`) |
| **Group** | A logical container for mailboxes, used for routing and team access |
| **Custom domain** | A domain you own that has been DNS-verified in Smailor |
| **Local part** | The part of an email address before `@` (e.g. `support` in `support@yourdomain.com`) |
| **Reputation** | Domain deliverability health score (0–100), monitored continuously |
| **Inbound webhook** | An HTTPS endpoint that receives a POST when a new email arrives |

---

## Support & Links

| Resource | URL |
|----------|-----|
| Dashboard | https://app.smailor.com |
| Documentation | https://app.smailor.com/docs |
| Legal | https://app.smailor.com/terms |
| Status page | Check dashboard for current system status |

---

*This file is machine-generated for AI context injection. Last updated: 2026-05-21. For the authoritative API reference, see https://app.smailor.com/docs.*
