# Emails

> Send transactional email through your verified domains.

Source: https://tickos.dev/docs/emails

The Emails resource sends one-to-one transactional email (receipts, notifications, password resets) from any verified domain connection. Emails are queued asynchronously via AWS SQS and return 202 Accepted immediately.

## POST /v1/emails -- Send an email

Permission: write.

| Field | Type | Required | Description |
|---|---|---|---|
| from | string | Yes | Sender email. The domain must be configured and verified for outbound email in Connections. |
| from_name | string | No | Sender display name. |
| to | string[] | Yes | Array of recipient email addresses. The maximum per request depends on your plan (see Limits below). |
| subject | string | Yes | Email subject line. |
| html | string | Conditional | HTML email body. Provide html OR text (or both). |
| text | string | Conditional | Plain text email body. |
| cc | string[] | No | CC recipients. |
| bcc | string[] | No | BCC recipients. |
| reply_to | string | No | Reply-to address. Applied to immediate sends only; scheduled emails (send_at) are currently sent without a Reply-To header, so replies go to the from address. |
| metadata | object | No | Arbitrary JSON metadata stored with the email. |
| track_opens | boolean | No | Enable open tracking pixel (requires html). Default: false. |
| track_clicks | boolean | No | Rewrite links for click tracking (requires html). Default: false. |
| type_sent | string | No | transactional (default) or broadcast. Determines the SQS queue. |
| files | array | No | Attachments: [{ fileName, fileData (base64), contentType }]. Max 5 files per email, 10 MB per file. |
| send_at | string | No | ISO 8601 date to schedule the email. Must be in the future and at most 30 days ahead. If omitted, the email sends immediately. |

```bash
curl https://api.tickos.dev/v1/emails \
  -H 'Authorization: Bearer sk_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{
    "from": "support@yourdomain.com",
    "from_name": "Acme Support",
    "to": ["jane@example.com"],
    "subject": "Your invoice is ready",
    "html": "<p>Hi Jane, your invoice is attached.</p>",
    "track_opens": true,
    "send_at": "2026-07-15T10:00:00-06:00"
  }'
```

Returns 202 with { id, status: "queued", queued_at } for immediate emails, or { id, status: "scheduled", scheduled_at } for scheduled emails. Scheduled emails are processed by a cron job every minute.

At send time, scheduled emails are checked again: if the account is suspended, the send limits are reached or the primary recipient is suppressed, the email is marked blocked; if the sending domain is no longer verified, it is marked failed.

### Attachments

Each entry in files needs fileName, fileData (base64-encoded content) and contentType. Up to 5 files per email and 10 MB per file (more files, or a larger file, returns 400). Attachments are stored in your account storage: if the upload would exceed your plan storage quota, the request returns 429 with the limit-exceeded body described below.

### Limits

Every send is checked against your plan before it is accepted:

| Plan | Max recipients in to per request |
|---|---|
| Free | 10 |
| Starter | 50 |
| Pro | 100 |
| Enterprise | 200 |

The monthly email quota of your plan, plus daily and hourly sending limits, are also enforced. When a recipient count, send limit or storage quota is exceeded, the API returns 429 with this body:

```json
{
  "error": "Limit exceeded",
  "message": "Hourly email limit reached (30/30). Please wait before sending more emails.",
  "current": 30,
  "limit": 30,
  "percentage": 100,
  "recommended_plan": "starter"
}
```

If your account is suspended, sends return 403 with { "error": "Account suspended. Please contact support." }.

### Suppressed recipients

Before sending, every address in to is checked against its contact record. A recipient is rejected when the contact has 3 or more recorded bounces (metadata.bounce_count) or is marked undeliverable (metadata.email_deliverable set to "false"). The whole request is rejected with HTTP 422 -- no email is sent to any recipient:

```json
{
  "error": "Recipient blocked",
  "message": "Recipient jane@example.com has too many bounces (3)."
}
```

Only the to addresses are checked; cc and bcc addresses are not.

## GET /v1/emails/scheduled -- List scheduled emails

Permission: read. Returns emails with status "scheduled", ordered by scheduled_at ascending. Query params: limit (default 50, max 100) and offset.

Each item also includes cc_addresses, bcc_addresses, created_at, metadata, type_sent, track_opens and track_clicks.:

```json
{
  "data": [
    {
      "id": "email_abc123",
      "from_address": "support@yourdomain.com",
      "to_address": "jane@example.com",
      "subject": "Your invoice is ready",
      "status": "scheduled",
      "scheduled_at": "2026-07-15T16:00:00.000Z",
      "...": "..."
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
```

## PATCH /v1/emails/{id}/schedule -- Reschedule an email

Permission: write. Change the send_at date of a scheduled email. The new date must be in the future and at most 30 days ahead. Only works on emails with status "scheduled" (other statuses return 400). Returns { id, status: "scheduled", scheduled_at, message }.

```bash
curl -X PATCH https://api.tickos.dev/v1/emails/email_abc123/schedule \
  -H 'Authorization: Bearer sk_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{ "send_at": "2026-07-20T09:00:00-06:00" }'
```

## DELETE /v1/emails/{id}/schedule -- Cancel a scheduled email

Permission: write. Cancels a scheduled email before it is sent. Only works on emails with status "scheduled" (other statuses return 400).

The response is { id, status: "cancelled", message: "Scheduled email cancelled successfully" }. Note that the stored email is marked as failed with error_message "Cancelled by user": a later GET /v1/emails/{id} returns status "failed", not "cancelled".

## GET /v1/emails/{id} -- Get email status

Permission: read. Returns the current delivery state of a single email, including bounce details when applicable. Useful for polling after a send. The response contains id, status, from, to (primary recipient), cc, subject, type_sent, created_at and the fields below.

| Field | Type | Description |
|---|---|---|
| status | string | scheduled, queued, processing, sent, delivered, bounced, failed or blocked. Cancelled scheduled emails show failed. |
| queued_at | string \| null | When the email entered the send queue. |
| scheduled_at | string \| null | When the email is scheduled to send (scheduled emails). |
| delivered_at | string \| null | Delivery confirmation timestamp. |
| bounced_at | string \| null | Bounce timestamp, if any. |
| error_message | string \| null | Failure reason when status is failed (for example "Cancelled by user"). |
| bounce | object \| null | { type, sub_type, recipients } when the email bounced. |

## POST /v1/emails/template -- Send from a template

Permission: write. Sends an email using a saved template with variable substitution. Template sends are always immediate and transactional: this endpoint does not accept send_at, files, text or type_sent. The same plan limits, suppression checks and 403/422/429 responses as POST /v1/emails apply.

| Field | Type | Required | Description |
|---|---|---|---|
| template_id | string | Yes | Template ID (e.g. etpl_xxxxxxxx). Unknown IDs return 404. |
| to | string[] | Yes | Array of recipient email addresses. |
| variables | object | No | Custom variable values as strings: { "order_id": "A-1001" }. See precedence below. |
| from | string | No | Override the template sender email. Required if the template has none. |
| from_name | string | No | Override the template sender name. |
| subject | string | No | Override the template subject. Required if the template has none. |
| reply_to | string | No | Override the template reply-to address. |
| cc | string[] | No | CC recipients. |
| bcc | string[] | No | BCC recipients. |
| track_opens | boolean | No | Default: false. |
| track_clicks | boolean | No | Default: false. |
| metadata | object | No | Stored with the email. TickOS adds template_id and template_name to it. |

```bash
curl https://api.tickos.dev/v1/emails/template \
  -H 'Authorization: Bearer sk_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{
    "template_id": "etpl_xxxxxxxx",
    "to": ["jane@example.com"],
    "variables": { "order_id": "A-1001", "plan_name": "Pro" }
  }'
```

Returns 202 with { id, status: "queued", template_id, template_name, queued_at }.

### Template variables

Variables use {{key}} syntax with no spaces inside the braces ({{ order_id }} is not replaced). Values are HTML-escaped and replaced in both the HTML body and the subject. Variables are resolved in this order:

1. System variables: name, first_name, email and phone. They are always resolved from the contact record of the first address in to (empty string if unknown; email falls back to the recipient address).
2. Custom variables from the variables object in the request.
3. Default values defined in the template.

> **Reserved names:** Because system variables are resolved first, custom values for name, first_name, email or phone are ignored. Use different keys (for example customer_name) for your own values. Variables with no value at any level are left as-is in the output. All recipients receive the content resolved for the first address in to.

## GET /v1/emails/stats -- Email statistics

Permission: read. Returns delivery, tracking and health metrics.

| Query param | Description |
|---|---|
| start_date | ISO 8601 date. Only emails created on or after this date are counted. Default: all time. |
| include_timeline | Set to true to add a timeline object: { days: [{ date, received, sent, broadcast }] }, a daily series (UTC) of inbound, transactional and campaign emails. |

The response contains total, sent, received, byStatus { delivered, sent, pending, failed }, tracking { emailsWithTracking, uniqueOpens, uniqueClicks, openRate, clickRate } and health (last 7 days: totalSent7d, totalDelivered7d, totalBounced7d, totalComplained7d, bounceRate, complaintRate, deliveryRate, plus hard/soft bounce breakdown fields that are null when not available).

> **Suppressions:** Recipients whose contact has 3 or more recorded bounces, or is marked undeliverable, are rejected. Sending to such an address in to returns HTTP 422 and nothing is sent. See Suppressed recipients above.
