# Email API

Read, search, send, reply to and draft mail in your users' Gmail, Outlook and IMAP mailboxes with one set of /v1/email endpoints.

import { Tab, Tabs } from 'fumadocs-ui/components/tabs';

The Email API works inside your users' own mailboxes. A user connects Gmail, Outlook or any IMAP mailbox once. Then you read, search, send, reply and draft through the same `/v1/email` calls, whatever the provider.

## What you can build

- **An AI assistant that drafts replies for approval.** Read a message with `clean=true` through `GET /v1/email/messages/{messageId}`, save an answer with `POST /v1/email/drafts`, and send it after a person approves.
- **A CRM that logs every thread.** List threads per contact with `GET /v1/email/threads?anyEmail=...`, and get new mail as [`message.received`](/webhooks/inbox/message-received) webhooks.
- **Personal outreach from the user's own address.** Send with `POST /v1/email/messages`, schedule with `scheduledFor`, and track opens, clicks and replies.
- **A support tool that answers in the thread.** Reply with `POST /v1/email/messages/{messageId}/reply` or forward with `POST /v1/email/messages/{messageId}/forward`.
- **Inbox triage.** Label, move, archive or mark mail read with `PATCH /v1/email/messages/{messageId}`.

## Providers

<ProductPlatforms product="email" />

The calls are the same on all three. The provider decides the daily send cap and how mail is organized:

| Provider | Mailboxes | Daily sends | Organize with |
|---|---|---|---|
| <PlatformName slug="google" page="email" /> | Gmail and Google Workspace | 200, or 500 on Workspace | Labels |
| <PlatformName slug="microsoft" page="email" /> | Microsoft 365 and Outlook.com | 1,000 | Folders and categories |
| <PlatformName slug="imap" page="email" /> | iCloud, Yahoo, Fastmail, GMX, Zoho and any IMAP server | 200 | Folders only |

## How it works

A mailbox is a connected account with its own `accountId`. Connect Google or Microsoft with OAuth and `email` in `scopes`. For any provider, send the user to the hosted page from `GET /v1/connect/email` ([IMAP connect](/platforms/imap/connect)).

Every call takes that `accountId`. Message and thread ids are the provider's own, so you can store them next to your records.

Reads are live calls to the provider and are not metered. Sent mail lands in the user's Sent folder.

Mail received, and mail sent through the API, also shows up as [inbox](/messages/list-inbox-conversations) conversations, one per thread.

Each mailbox counts as one [connected account](/pricing/social), whatever areas it grants. Sending from Gmail or IMAP needs a payment method on the workspace.

## Your first request

You need an API key and the `accountId` of a connected mailbox. This sends one email from it.

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
import Zernio from '@zernio/node';

const zernio = new Zernio();

const { data } = await zernio.email.sendEmailMessage({
  body: {
    accountId: '66b2e19d8c3f5a7e9d0b1c2d',
    to: [{ email: 'client@example.com', name: 'Client' }],
    subject: 'Your booking',
    html: '<p>See you on Tuesday.</p>',
  },
});

console.log(data.data.id, data.data.threadId);
```
</Tab>
<Tab value="Python">
```python
from zernio import Zernio

client = Zernio()

sent = client.email.send_email_message(
    account_id="66b2e19d8c3f5a7e9d0b1c2d",
    to=[{"email": "client@example.com", "name": "Client"}],
    subject="Your booking",
    html="<p>See you on Tuesday.</p>",
)

print(sent["data"]["id"], sent["data"]["threadId"])
```
</Tab>
<Tab value="curl">
```bash
curl -X POST "https://zernio.com/api/v1/email/messages" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "to": [{ "email": "client@example.com", "name": "Client" }],
    "subject": "Your booking",
    "html": "<p>See you on Tuesday.</p>"
  }'
```
</Tab>
</Tabs>

Response (`201`), the sent copy from the mailbox:

```json
{
  "data": {
    "id": "18f2c0a9d3b1e7f4",
    "threadId": "18f2c0a9d3b1e7f4",
    "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
    "provider": "google",
    "from": { "email": "ana@example.com" },
    "to": [{ "email": "client@example.com", "name": "Client" }],
    "subject": "Your booking"
  }
}
```

Every field of a send, such as `scheduledFor`, `attachments` and tracking, is on [Send an email](/email/send-email-message).

## Examples

### Search and read for an LLM

Filter with `from`, `subject`, `hasAttachment`, `after` and `before`. Then read one message with `clean=true` to get its new text without quoted replies or signatures.

```bash
curl "https://zernio.com/api/v1/email/messages?accountId=66b2e19d8c3f5a7e9d0b1c2d&from=billing@acme.com&hasAttachment=true&limit=20" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"

curl "https://zernio.com/api/v1/email/messages/18f2c0a9d3b1e7f4?accountId=66b2e19d8c3f5a7e9d0b1c2d&clean=true" \
  -H "Authorization: Bearer $ZERNIO_API_KEY"
```

List rows carry no bodies. The read returns `text`, `html` and a `clean` object with `clean.text` ready for a model. Pass `pagination.nextCursor` back as `cursor` for the next page.

### Draft a reply for approval

`inReplyTo` makes a reply draft in that thread. It lives in the mailbox, so the user sees and edits it in their mail app before anyone sends it.

<Tabs items={['Node.js', 'Python', 'curl']}>
<Tab value="Node.js">
```typescript
const { data: draft } = await zernio.email.createEmailDraft({
  body: {
    accountId: '66b2e19d8c3f5a7e9d0b1c2d',
    inReplyTo: '18f2c0a9d3b1e7f4',
    html: '<p>Tuesday at 10:00 works. See you then.</p>',
  },
});

// After a person approves this version:
await zernio.email.sendEmailDraft({
  path: { draftId: draft.data.id },
  headers: { 'If-Match': draft.data.version },
  body: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
});
```
</Tab>
<Tab value="Python">
```python
draft = client.email.create_email_draft(
    account_id="66b2e19d8c3f5a7e9d0b1c2d",
    in_reply_to="18f2c0a9d3b1e7f4",
    html="<p>Tuesday at 10:00 works. See you then.</p>",
)

# After a person approves this version:
client.email.send_email_draft(
    draft_id=draft["data"]["id"],
    account_id="66b2e19d8c3f5a7e9d0b1c2d",
    if_match=draft["data"]["version"],
)
```
</Tab>
<Tab value="curl">
```bash
curl -X POST "https://zernio.com/api/v1/email/drafts" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "accountId": "66b2e19d8c3f5a7e9d0b1c2d", "inReplyTo": "18f2c0a9d3b1e7f4", "html": "<p>Tuesday at 10:00 works. See you then.</p>" }'

curl -X POST "https://zernio.com/api/v1/email/drafts/DRAFT_ID/send" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "If-Match: \"VERSION\"" \
  -d '{ "accountId": "66b2e19d8c3f5a7e9d0b1c2d" }'
```
</Tab>
</Tabs>

The draft's `version` changes on every edit. With it in `If-Match`, the send answers `412` `draft_conflict` if someone edited the draft since, instead of sending text nobody approved.

### Reply in the thread

A reply goes to the original sender, or to everyone with `mode: "replyAll"`. The provider keeps the thread and the subject.

```bash
curl -X POST "https://zernio.com/api/v1/email/messages/18f2c0a9d3b1e7f4/reply" \
  -H "Authorization: Bearer $ZERNIO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "accountId": "66b2e19d8c3f5a7e9d0b1c2d", "mode": "replyAll", "text": "Thanks, confirmed." }'
```

Replies count against the same daily cap as sends. More on [Reply to a message](/email/reply-email-message).

## How it behaves

- **One-to-one mail.** The same body sent to more than 50 recipients within an hour pauses the mailbox and fires `account.rate_limited`. Use it for personal mail, not newsletters.
- **Send cap headers.** Every send answers with `X-Zernio-Send-Limit`, `X-Zernio-Send-Remaining` and `X-Zernio-Send-Reset`. The cap resets at midnight UTC.
- **Tracking.** `trackOpens`, `trackLinks` and `trackReplies` fire `email.opened`, `email.link_clicked` and `email.thread_replied`. Bounces fire `email.bounced`.
- **Rate limit.** Reads count against your [API rate limit](/guides/rate-limits).

## If it fails

| Error | Cause | Fix |
|---|---|---|
| `402` `payment_required` | No payment method, on a Gmail or IMAP send | Add a card in billing, then send again |
| `412` `account_scope_missing` | The account was connected without `email` | Connect it again with `email` in `scopes` |
| `412` `draft_conflict` | The draft changed since the version in `If-Match` | Review `details.draft`, then send its new `version` |
| `429` `email_send_cap_reached` | The mailbox reached today's cap | Wait for `Retry-After` |
| `400` on labels | IMAP mailboxes have no labels | Move mail with `folder` instead |

Every email error and its fix is in [email errors](/guides/email-errors).

## Related

- [Google email](/platforms/google/email)
- [Microsoft email](/platforms/microsoft/email)
- [IMAP email](/platforms/imap/email)
- [Send an email](/email/send-email-message)
- [Create a draft](/email/create-email-draft)
- [Inbox webhooks](/webhooks/inbox)