# MakiSend API reference: send email, templates and logs

> Serverless transactional email API on Cloudflare Workers. Base URL: your own instance, `https://<your-worker>.workers.dev`. OpenAPI 3.1: https://makisend.lixalistudio.tech/openapi.json. Each instance also serves its full reference at `/docs`.

MakiSend accepts transactional emails over HTTP, stores their content encrypted in D1, and delivers them asynchronously through Cloudflare Queues, Workflows, and SMTP.

**Authentication.** `POST /send-email` uses a per-tenant API key from the dashboard, sent as `x-api-key` or `Authorization: Bearer <key>`. Dashboard endpoints use the Better Auth session cookie created by `POST /api/auth/sign-in/email`; open the dashboard and sign in before trying those routes here.

**Delivery is asynchronous.** `POST /send-email` returns `202 Accepted` once the email is persisted and queued. Delivery status is available through `GET /email-logs` and the dashboard endpoints.

**This page documents the email API of a self-hosted MakiSend instance.** Replace `your-instance.workers.dev` with your Worker's address. Each instance also serves its full reference, dashboard routes included, at `/docs`.

## Example

```bash
curl -X POST https://<your-worker>.workers.dev/send-email \
  -H "Authorization: Bearer $MAKISEND_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042" \
  -d '{
    "to": "customer@example.com",
    "subject": "Your receipt",
    "text": "Thanks for order #1042.",
    "html": "<p>Thanks for order <b>#1042</b>.</p>"
  }'
```

## POST /send-email

Queue a transactional email. Accepts either inline content (`subject`, `text`, `html`) or a `templateId` plus `variables`. The two shapes are mutually exclusive. HTML is sanitized before delivery. Send the optional `Idempotency-Key` header to make retries safe.

Auth: API key: `Authorization: Bearer <api-key>` or `x-api-key: <api-key>`.

### Parameters

| Name | In, type | Required | Notes |
|---|---|---|---|
| `Idempotency-Key` | header, string | no | 1 to 255 characters. Optional unique key for safe retries. Replaying the same key and body returns the stored response for 24 hours; reusing it with a different body returns 409 |

### Request body

Inline email content, or a template reference. The shapes are mutually exclusive.

#### Inline content

| Field | Type | Required | Notes |
|---|---|---|---|
| `to` | string (email) | yes | At most 320 characters |
| `subject` | string | yes | 3 to 500 characters |
| `text` | string | yes | 10 to 20000 characters |
| `html` | string | yes | 1 to 100000 characters |
| `attachments` | array of object | no | At most 5 items. Each item: `filename`, `content`, `content_type` (optional) |

#### From a saved template

| Field | Type | Required | Notes |
|---|---|---|---|
| `to` | string (email) | yes | At most 320 characters |
| `templateId` | string | yes | 1 to 128 characters |
| `variables` | object | no | Values: string \| number \| boolean |
| `attachments` | array of object | no | At most 5 items. Each item: `filename`, `content`, `content_type` (optional) |

### Responses

- `202`: Email persisted and queued for asynchronous delivery.
- `400`: Request validation failed.
- `401`: Missing or invalid credentials.
- `403`: Credentials are valid but the resource is not accessible.
- `404`: The referenced template does not exist for this account.
- `409`: Idempotency key conflict.
- `429`: Only when the instance sets `SEND_RATE_LIMIT`: this API key reached its send quota. `Retry-After` gives the seconds until the window restarts.
- `500`: Unexpected server error.

## GET /email-logs

List email logs. Newest first, cursor-paginated. The recipient filter is applied to the decrypted current page, so a page can contain fewer rows than `limit` when filtering by `to`.

Auth: Dashboard session (sign in to your instance first).

### Parameters

| Name | In, type | Required | Notes |
|---|---|---|---|
| `smtpConfigId` | query, string | no | At most 128 characters. The emails sent with this configuration's keys, plus its test sends |
| `apiKeyId` | query, string | no | At most 128 characters. Only the emails sent with this API key |
| `to` | query, string | no | At most 320 characters. Case-insensitive substring match on the decrypted recipient |
| `status` | query, string | no | One of `received`, `processing`, `cleaned`, `sent`, `failed` |
| `limit` | query, integer | no | 1 to 100. Default `50` |
| `cursor` | query, string | no | At most 512 characters. Opaque `nextCursor` from the previous page |

### Responses

- `200`: Page of email logs with their history.
- `401`: Missing or invalid credentials.
- `403`: Credentials are valid but the resource is not accessible.
- `500`: Unexpected server error.

## GET /email-logs/{id}

Get an email log.

Auth: Dashboard session (sign in to your instance first).

### Parameters

| Name | In, type | Required | Notes |
|---|---|---|---|
| `id` | path, string | yes | 1 to 128 characters. Resource identifier |

### Responses

- `200`: Email log with its full history.
- `401`: Missing or invalid credentials.
- `403`: Credentials are valid but the resource is not accessible.
- `404`: Resource not found.
- `500`: Unexpected server error.

## GET /templates

List templates.

Auth: Dashboard session (sign in to your instance first).

### Responses

- `200`: Templates owned by the session user.
- `401`: Missing or invalid credentials.
- `403`: Credentials are valid but the resource is not accessible.
- `500`: Unexpected server error.

## POST /templates

Create a template. Use `{{variable}}` placeholders in the subject, HTML, or text. Variables are supplied at send time.

Auth: Dashboard session (sign in to your instance first).

### Request body

New reusable email template.

#### JSON body

| Field | Type | Required | Notes |
|---|---|---|---|
| `name` | string | yes | 1 to 100 characters |
| `subject` | string | yes | 1 to 500 characters |
| `format` | string | no | One of `legacy`, `react-email`, `react-email-code`. Default `legacy` |
| `editorJson` | string | no | 2 to 250000 characters |
| `source` | string | no | 1 to 100000 characters |
| `html` | string | yes | 1 to 100000 characters |
| `text` | string | yes | 1 to 20000 characters |

### Responses

- `201`: Template created.
- `400`: Request validation failed.
- `401`: Missing or invalid credentials.
- `403`: Credentials are valid but the resource is not accessible.
- `500`: Unexpected server error.

## GET /templates/{id}

Get a template.

Auth: Dashboard session (sign in to your instance first).

### Parameters

| Name | In, type | Required | Notes |
|---|---|---|---|
| `id` | path, string | yes | 1 to 128 characters. Resource identifier |

### Responses

- `200`: Template.
- `401`: Missing or invalid credentials.
- `403`: Credentials are valid but the resource is not accessible.
- `404`: Resource not found.
- `500`: Unexpected server error.

## PUT /templates/{id}

Update a template.

Auth: Dashboard session (sign in to your instance first).

### Parameters

| Name | In, type | Required | Notes |
|---|---|---|---|
| `id` | path, string | yes | 1 to 128 characters. Resource identifier |

### Request body

Partial template update.

#### JSON body

| Field | Type | Required | Notes |
|---|---|---|---|
| `name` | string | no | 1 to 100 characters |
| `subject` | string | no | 1 to 500 characters |
| `format` | string | no | One of `legacy`, `react-email`, `react-email-code` |
| `editorJson` | object | no |  |
| `source` | object | no |  |
| `html` | string | no | 1 to 100000 characters |
| `text` | string | no | 1 to 20000 characters |

### Responses

- `200`: Template updated.
- `400`: Request validation failed.
- `401`: Missing or invalid credentials.
- `403`: Credentials are valid but the resource is not accessible.
- `404`: Resource not found.
- `500`: Unexpected server error.

## DELETE /templates/{id}

Delete a template.

Auth: Dashboard session (sign in to your instance first).

### Parameters

| Name | In, type | Required | Notes |
|---|---|---|---|
| `id` | path, string | yes | 1 to 128 characters. Resource identifier |

### Responses

- `200`: Template deleted.
- `401`: Missing or invalid credentials.
- `403`: Credentials are valid but the resource is not accessible.
- `404`: Resource not found.
- `500`: Unexpected server error.

## POST /templates/{id}/preview

Preview a template. Renders the saved subject, HTML, and text with the supplied variables. Missing variables, line breaks, and length limits are reported exactly as they would be by `POST /send-email`.

Auth: Dashboard session (sign in to your instance first).

### Parameters

| Name | In, type | Required | Notes |
|---|---|---|---|
| `id` | path, string | yes | 1 to 128 characters. Resource identifier |

### Request body

Variable values used to render a saved template preview.

#### JSON body

| Field | Type | Required | Notes |
|---|---|---|---|
| `variables` | object | no | Values: string \| number \| boolean |

### Responses

- `200`: Rendered template preview.
- `400`: Request validation failed.
- `401`: Missing or invalid credentials.
- `403`: Credentials are valid but the resource is not accessible.
- `404`: Resource not found.
- `500`: Unexpected server error.
