# Emelia Inbox API

> The Emelia Inbox API lets your code or an AI agent list, search and read the emails of your mailboxes, label them, prepare drafts and send replies. Same API key as the Emelia API, its own quotas.

Source: https://docs.emelia.io/docs/inbox-api

The Emelia Inbox API is the programmatic side of the Emelia unified inbox (the **Inbox** tab of the app). It lets your code or an AI agent list, search and read the emails of the mailboxes connected to Emelia, organise them with labels, AI label rules, saved views and inbox filters, prepare drafts for a human to review, send replies, and forward emails automatically.

## Base URL and key

The Inbox API lives on the same host as the Emelia API, under `/inbox/api/v1`, and takes the same API key: the one you create in [Settings, API & Webhooks](https://docs.emelia.io/docs/authentication.md).

|          | Emelia API                                | Emelia Inbox API                     |
| -------- | ----------------------------------------- | ------------------------------------ |
| Base URL | `https://api.emelia.io`                   | `https://api.emelia.io/inbox/api/v1` |
| Covers   | Campaigns, contacts, enrichment, LinkedIn | The emails of the inbox              |
| Errors   | `{ "error", "code"? }`                    | `{ "code", "error", "details"? }`    |
| Quotas   | See [Rate limits](https://docs.emelia.io/docs/rate-limits.md)      | See [Quotas](#quotas) below          |

The full contract is also available as an OpenAPI file: [/openapi/inbox.json](https://docs.emelia.io/openapi/inbox.json).

## Authentication

Send your API key in the `Authorization` header on every call, as is (a `Bearer ` prefix is accepted too). The workspace comes from the key: no path carries a workspace id, so a key only reaches the inbox of its own Emelia account.

> **The key can send emails.** As on the rest of the Emelia API, a key has every right: it reads every email of the inbox, and it can send emails, create auto-forward rules and turn on auto-reply. Give one key to each integration, and keep it on your server.

An unknown or disabled key gets `401 INVALID_API_KEY`. Disabling a key in Emelia can take up to 5 minutes to apply to the Inbox API. A workspace whose Emelia subscription ended gets `403 WORKSPACE_SUSPENDED`.

## List and read emails

[`GET /emails`](https://docs.emelia.io/docs/inbox-api/list-emails.md)

**curl**

```bash
curl "https://api.emelia.io/inbox/api/v1/emails?folderType=INBOX&limit=50" \
  -H "Authorization: $EMELIA_API_KEY"
```

**JavaScript**

```js
const API = 'https://api.emelia.io/inbox/api/v1';
const headers = { Authorization: process.env.EMELIA_API_KEY };

// Every incoming email, page by page
let cursor;
do {
  const params = new URLSearchParams({ folderType: 'INBOX', limit: '200', ...(cursor ? { cursor } : {}) });
  const res = await fetch(`${API}/emails?${params}`, { headers });
  const page = await res.json();
  for (const email of page.items) console.log(email.subject);
  cursor = page.hasMore ? page.nextCursor : undefined;
} while (cursor);
```

**Python**

```python
import os
import requests

API = "https://api.emelia.io/inbox/api/v1"
session = requests.Session()
session.headers["Authorization"] = os.environ["EMELIA_API_KEY"]

# Every incoming email, page by page
cursor = None
while True:
    params = {"folderType": "INBOX", "limit": 200, **({"cursor": cursor} if cursor else {})}
    page = session.get(f"{API}/emails", params=params).json()
    for email in page["items"]:
        print(email["subject"])
    if not page["hasMore"]:
        break
    cursor = page["nextCursor"]
```

* **Pagination**: pass `nextCursor` back as `cursor`, and stop when `hasMore` is `false`. `nextCursor` is only `null` on an empty page, so do not stop on it. `limit` is 50 by default, 200 at most.
* **Folders**: without `folderType`, every folder is listed, spam and trash included. With `folderType=INBOX`, the emails hidden by inbox filters are left out; `filtered=only` returns them instead.
* **Body**: lists never include the body. Read it, with the whole conversation, with [Get the body and thread](https://docs.emelia.io/docs/inbox-api/get-body-and-thread.md).
* **Search**: [Search emails by keyword](https://docs.emelia.io/docs/inbox-api/search-emails-by-keyword.md) takes 2 to 200 characters. Known issue: a `search` parameter on `GET /emails` ignores `filters` and `filtered`; do not combine them.
* Every read is cancelled after 10 seconds (`408 QUERY_TIMEOUT`).

## Send a reply

[`POST /drafts/send`](https://docs.emelia.io/docs/inbox-api/send-email-now.md)

**curl**

```bash
curl -X POST https://api.emelia.io/inbox/api/v1/drafts/send \
  -H "Authorization: $EMELIA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: reply-claire-2026-10-13" \
  -d '{
    "mailboxId": "cmbxacme0000000000000001",
    "to": [{ "email": "claire.fontaine@nexora.io", "name": "Claire Fontaine" }],
    "subject": "Re: Quick question about Nexora",
    "inReplyTo": "<CAF7x2y3z@mail.nexora.io>",
    "references": ["<CAF7x2y3z@mail.nexora.io>"],
    "body": { "text": "Hi Claire,\n\nTuesday 10am works for me.", "html": "<p>Hi Claire,</p><p>Tuesday 10am works for me.</p>" }
  }'
```

**JavaScript**

```js
const res = await fetch(`${API}/drafts/send`, {
  method: 'POST',
  headers: { ...headers, 'Content-Type': 'application/json', 'Idempotency-Key': 'reply-claire-2026-10-13' },
  body: JSON.stringify({
    mailboxId: 'cmbxacme0000000000000001',
    to: [{ email: 'claire.fontaine@nexora.io', name: 'Claire Fontaine' }],
    subject: 'Re: Quick question about Nexora',
    inReplyTo: '<CAF7x2y3z@mail.nexora.io>',
    references: ['<CAF7x2y3z@mail.nexora.io>'],
    body: { text: 'Hi Claire,\n\nTuesday 10am works for me.', html: '<p>Hi Claire,</p><p>Tuesday 10am works for me.</p>' },
  }),
});
console.log(res.status, await res.json()); // 201 { messageId, threadId }
```

**Python**

```python
res = session.post(
    f"{API}/drafts/send",
    headers={"Idempotency-Key": "reply-claire-2026-10-13"},
    json={
        "mailboxId": "cmbxacme0000000000000001",
        "to": [{"email": "claire.fontaine@nexora.io", "name": "Claire Fontaine"}],
        "subject": "Re: Quick question about Nexora",
        "inReplyTo": "<CAF7x2y3z@mail.nexora.io>",
        "references": ["<CAF7x2y3z@mail.nexora.io>"],
        "body": {"text": "Hi Claire,\n\nTuesday 10am works for me.", "html": "<p>Hi Claire,</p><p>Tuesday 10am works for me.</p>"},
    },
)
print(res.status_code, res.json())  # 201 {"messageId": ..., "threadId": ...}
```

To reply in a thread: send from the `mailboxId` of the original email, set `inReplyTo` to its `messageId`, `references` to its `references` followed by its `messageId`, and prefix the subject with `Re: `. The signature of the mailbox is added by default. Attachments cannot be sent through the API.

> **Sending is irreversible.** Always send an `Idempotency-Key`. Known issue: if the message left but a later step failed, the API answers `500 INTERNAL_ERROR` and a retry sends the message again. After a `500`, look in the `SENT` folder before retrying. For a human in the loop, create a [draft](https://docs.emelia.io/docs/inbox-api/create-draft.md) instead: it waits in the inbox for review.

## Organise the inbox

| Need                                                                                          | Routes                                                                                                                                                                                    |
| --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Mark read, star, trash                                                                        | [Mark read or starred](https://docs.emelia.io/docs/inbox-api/mark-read-or-starred.md), [Move an email to the trash](https://docs.emelia.io/docs/inbox-api/move-email-to-trash.md), [Bulk update emails](https://docs.emelia.io/docs/inbox-api/bulk-update-emails.md) |
| Labels you apply yourself                                                                     | [Labels](https://docs.emelia.io/docs/inbox-api/list-labels.md), then [Apply a label to an email](https://docs.emelia.io/docs/inbox-api/apply-label-to-email.md)                                                                             |
| Labels applied by AI from a plain language condition, with optional auto-draft and auto-reply | [AI label rules](https://docs.emelia.io/docs/inbox-api/list-ai-label-rules.md)                                                                                                                                     |
| Saved filters, read with `GET /emails?viewId=`                                                | [Views](https://docs.emelia.io/docs/inbox-api/list-saved-views.md)                                                                                                                                                 |
| Hide emails from the main inbox without moving them                                           | [Inbox filters](https://docs.emelia.io/docs/inbox-api/list-inbox-filters.md)                                                                                                                                       |
| Forward matching emails automatically                                                         | [Auto-forward rules](https://docs.emelia.io/docs/inbox-api/list-auto-forward-rules.md)                                                                                                                             |

AI work triggered by the inbox (classification of each incoming email while an AI label rule exists, auto-drafts) is paid with Emelia credits.

## Quotas

Counted per Emelia account (every key shares them), over a sliding minute and a calendar month in UTC. They are separate from the [rate limits](https://docs.emelia.io/docs/rate-limits.md) of the Emelia API:

| Limit               | Per Emelia account |
| ------------------- | ------------------ |
| Requests per minute | 60                 |
| Requests per month  | 10,000             |
| Active webhooks     | 1                  |

One request costs 1 unit. `POST /emails/bulk` costs one unit per id, checked against the per minute limit too: send at most 60 ids per call. Units are counted when the request comes in, even if it then fails validation. Over a limit, the API answers `429 RATE_LIMIT_EXCEEDED` with `Retry-After`, or `429 QUOTA_EXCEEDED` for the month. Responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `X-RateLimit-Quota`, `X-RateLimit-Quota-Remaining` and `X-RateLimit-Quota-Reset`.

## Conventions

* JSON in and out, bodies of 256 KB at most (`413 PAYLOAD_TOO_LARGE`). Dates are ISO 8601 in UTC. Ids are opaque strings.
* Errors: `{ "code", "error", "details"? }`. Branch on `code`. A resource of another workspace answers `404`, never `403`. An email not found answers `NOT_FOUND` on some routes and `EMAIL_NOT_FOUND` on others: treat both the same.
* `Idempotency-Key` (255 characters at most) is supported on the creating `POST` routes. A successful response is kept 24 hours and replayed with `Idempotent-Replay: true`; the same key with another request answers `409 CONFLICT`. Keys are scoped to the workspace, not to the API key.
* An unknown path answers an HTML 404 page.

## Webhooks

Six events can be pushed to your endpoint: `email.received`, `email.updated`, `email.deleted`, `draft.sent`, `label.applied` and `mailbox.sync_failed`. Each delivery is a `POST` with `{ "event", "createdAt", "data" }`, an `X-Maylee-Event` header and an `X-Maylee-Signature` header (`t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">`, keyed with the webhook secret). Reject deliveries older than 300 seconds and compare signatures in constant time. A delivery succeeds on a `2xx` within 10 seconds; failures are retried 4 times over about 8 hours.

> **Registering a webhook.** Webhook subscriptions cannot be created with an API key, and Emelia has no screen for them yet. Contact [contact@emelia.io](mailto:contact@emelia.io) if you need one.

The payload of each event is on its page, under **Webhook events** in this section.
