# Email

List, search and read threads, send and reply from a connected Gmail or Outlook mailbox, manage folders and attachments, and stay inside the daily send cap.

Send and read email from your users' own Gmail or Outlook mailboxes. Mail goes out from their address, lands in their Sent folder and threads normally for the recipient. Every call takes the `accountId` of a Google or Microsoft account connected with the `email` area (see [Email, calendar & files](https://pozy.ai/docs/productivity)).

## Send an email

```bash cURL
curl -X POST https://api.pozy.ai/v1/email/messages \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "6710c501e4b0d2f1a8c95e77",
    "to": [{ "email": "jordan@client.example", "name": "Jordan Lee" }],
    "subject": "Your proposal for November",
    "html": "<p>Hi Jordan, the proposal is attached. Happy to walk you through it on Thursday.</p>",
    "attachments": [
      { "filename": "proposal.pdf", "contentType": "application/pdf", "url": "https://files.example.com/proposal-nov.pdf" }
    ],
    "metadata": { "dealId": "deal_4471" }
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/email/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    accountId: "6710c501e4b0d2f1a8c95e77",
    to: [{ email: "jordan@client.example", name: "Jordan Lee" }],
    subject: "Your proposal for November",
    html: "<p>Hi Jordan, the proposal is attached. Happy to walk you through it on Thursday.</p>",
    attachments: [{ filename: "proposal.pdf", contentType: "application/pdf", url: "https://files.example.com/proposal-nov.pdf" }],
    metadata: { dealId: "deal_4471" },
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.data.id, data.data.threadId);
```
```python Python
import os, requests

resp = requests.post(
    "https://api.pozy.ai/v1/email/messages",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "accountId": "6710c501e4b0d2f1a8c95e77",
        "to": [{"email": "jordan@client.example", "name": "Jordan Lee"}],
        "subject": "Your proposal for November",
        "html": "<p>Hi Jordan, the proposal is attached.</p>",
    },
)
sent = resp.json()["data"]
print(resp.headers.get("X-Pozy-Send-Remaining"))
```

The call returns `201` with the sent copy, including its `id` and `threadId`.

| Field | Rules |
| --- | --- |
| `to`, `cc`, `bcc` | Lists of `{ email, name? }`, 100 at most in each. `to` cannot be empty |
| `replyTo` | 5 addresses at most |
| `fromName` | The name recipients see. The sending address cannot change: it is the mailbox itself |
| `subject` | Required; 998 characters at most |
| `text`, `html` | Send one or both |
| `attachments` | 20 at most. Each is `{ filename, contentType }` plus either `url` or `base64`. Give one a `contentId` to show it inline from `html`. Pozy must be able to fetch a `url` in 20 seconds |
| `replyToMessageId` | Puts the email in the same thread as that message |
| `headers` | Extra headers; every name must begin with `X-` |
| `metadata` | Your own string keys and values, returned on the message |

The schema also lists `scheduledFor`, `holdAsDraft`, `trackOpens` and `trackLinks`. None of them works yet, and sending one gets `400`.

## Reply

A reply stays in the original thread; Pozy adds the headers that keep it there. By default it goes back to the sender, and `mode: "replyAll"` includes everyone on the original. `to` and `subject` are optional and fall back to the original message's.

```bash cURL
curl -X POST https://api.pozy.ai/v1/email/messages/18f2c0a9d3b1e7f4/reply \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "accountId": "6710c501e4b0d2f1a8c95e77", "mode": "replyAll", "text": "Thursday at 10:00 works. Talk then." }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/email/messages/18f2c0a9d3b1e7f4/reply", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ accountId: "6710c501e4b0d2f1a8c95e77", mode: "replyAll", text: "Thursday at 10:00 works. Talk then." }),
});
if (!res.ok) throw new Error((await res.json()).error);
```
```python Python
requests.post(
    "https://api.pozy.ai/v1/email/messages/18f2c0a9d3b1e7f4/reply",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={"accountId": "6710c501e4b0d2f1a8c95e77", "mode": "replyAll", "text": "Thursday at 10:00 works."},
)
```

Outlook quotes the original below your reply. Replies count against the daily send cap like any send.

## List and search

`GET /v1/email/threads` returns threads, `GET /v1/email/messages` returns single messages. Both are newest first, read the inbox unless you pass `folder`, and page with `cursor` and `limit` (up to 200, default 50).

```bash cURL
curl "https://api.pozy.ai/v1/email/threads?accountId=6710c501e4b0d2f1a8c95e77&folder=inbox&unread=true&from=jordan@client.example&limit=20" \
  -H "Authorization: Bearer $POZY_API_KEY"
```

| Parameter | What it does |
| --- | --- |
| `folder` | One of `inbox`, `sent`, `drafts`, `trash`, `spam` and `archive`, or the id of a folder or label listed by `GET /v1/email/folders` |
| `unread`, `starred`, `hasAttachment` | `true` or `false` |
| `from`, `to`, `cc`, `bcc`, `anyEmail` | An email address; `anyEmail` matches it in any of those fields |
| `subject` | Text to find in the subject line |
| `after`, `before` | ISO 8601 dates that bound the message date |
| `searchQueryNative` | A query in the mail provider's own syntax, up to 500 characters, sent through untouched. Gmail: `has:attachment larger:5M`. Outlook: `subject:invoice` |
| `sortOrder` | `desc`, the default, or `asc` |

Some filters a provider cannot run on its side. Pozy then filters each page after fetching it and lists those filters in `meta.filteredClientSide`. A page filtered that way may come back with fewer than `limit` items even though `pagination.hasMore` is still `true`: page on until `hasMore` turns `false`. Outlook does not mix its native search with other filters, so with `searchQueryNative` set, the other filters are applied by Pozy as well.

## Open threads and messages

- `GET /v1/email/threads/{threadId}?accountId=...` gives you a thread and every message in it.
- `GET /v1/email/messages/{messageId}?accountId=...` returns a single message, including `text`, `html` and `attachments`. `includeHeaders=true` adds the headers; `raw=true` returns the whole MIME message.
- `GET /v1/email/attachments/{attachmentId}?accountId=...&messageId=...` downloads one attachment, sent with its filename and content type.

## Organise mail

To mark, flag or file mail, send `accountId` with any of `unread`, `starred` and `folder` to `PATCH /v1/email/messages/{messageId}` or `PATCH /v1/email/threads/{threadId}`. On Outlook, `starred` is the flag. A new `folder` moves the mail there; `"folder": "archive"` is how you archive. `DELETE /v1/email/messages/{messageId}?accountId=...` sends the message to the trash, which Outlook calls Deleted Items.

## Send caps and cooldowns

| Limit | Gmail | Google Workspace | Outlook |
| --- | --- | --- | --- |
| Sends a day per mailbox | 200 | 500 | 1,000 |
| Attachment size | 25 MB in total | 25 MB in total | 3 MB each, 25 MB in total |

The count starts again at midnight UTC. A reply is a send, and so is an attempt the provider turns down. Each send response carries three headers to help you pace:

| Header | Meaning |
| --- | --- |
| `X-Pozy-Send-Limit` | Today's cap for this mailbox |
| `X-Pozy-Send-Remaining` | Sends left today |
| `X-Pozy-Send-Reset` | When the cap resets |

`GET /v1/accounts/{accountId}/health` reports the same as `sendCap`.

Use these mailboxes for personal, one-to-one email, not bulk sends. If one body goes to over 50 different recipients inside an hour, the mailbox is paused for 10 minutes. Each further pause lasts twice as long as the one before, up to 24 hours. The start of a pause fires `account.rate_limited`:

```json
{
  "id": "evt_2b7f0c1d9e8a4f36",
  "event": "account.rate_limited",
  "timestamp": "2026-10-30T11:20:00.000Z",
  "account": {
    "accountId": "6710c501e4b0d2f1a8c95e77",
    "profileId": "6710c3a9e4b0d2f1a8c95e21",
    "platform": "google",
    "username": "ana@example.com",
    "reason": "recipient_burst",
    "cooldownMinutes": 10,
    "resetsAt": "2026-10-30T11:30:00.000Z"
  }
}
```

`reason` is `recipient_burst`, `provider_rejection`, `bounces` or `daily_cap`. For newsletters and bulk sends, use [broadcasts](https://pozy.ai/docs/automations/broadcasts) on a messaging channel instead.

## Email in the inbox

Mail received while the account is connected, and mail you send through the API, also appears as [inbox](https://pozy.ai/docs/inbox/messages) conversations (one per thread) and fires `message.received` or `message.sent`. The subject, `cc` and other email fields are under `metadata.email` on the inbox message. Older mail is reachable through the endpoints on this page, not the inbox.

## Errors

| Error | Cause | Fix |
| --- | --- | --- |
| `400 invalid_field_value` | A bad field: a non-`X-` header, attachments over the size limit, `scheduledFor` | Fix the field named in `param` |
| `401 reconnect_required` | The provider refused the account's access | Reconnect the account |
| `412 account_scope_missing` | The account has no `email` area | Reconnect with `email` in `scopes` |
| `412 admin_consent_required` | A Microsoft tenant needs admin approval | See [admin consent](https://pozy.ai/docs/platforms/microsoft#admin-consent) |
| `429 email_send_cap_reached` | Today's cap is used. `details.cap` has `limit`, `used` and `resetsAt` | Wait for `resetsAt` |
| `429 rate_limited` | A cooldown after a burst, or provider throttling | Wait for `Retry-After` and send fewer identical emails |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/email/threads`](https://pozy.ai/docs/api/listEmailThreads) | List email threads |
| [`GET /v1/email/threads/{threadId}`](https://pozy.ai/docs/api/getEmailThread) | Get an email thread with its messages |
| [`PATCH /v1/email/threads/{threadId}`](https://pozy.ai/docs/api/updateEmailThread) | Update every message in a thread |
| [`GET /v1/email/messages`](https://pozy.ai/docs/api/listEmailMessages) | List email messages |
| [`POST /v1/email/messages`](https://pozy.ai/docs/api/sendEmailMessage) | Send an email |
| [`GET /v1/email/messages/{messageId}`](https://pozy.ai/docs/api/getEmailMessage) | Get an email message |
| [`PATCH /v1/email/messages/{messageId}`](https://pozy.ai/docs/api/updateEmailMessage) | Mark read or starred, or move a message |
| [`DELETE /v1/email/messages/{messageId}`](https://pozy.ai/docs/api/trashEmailMessage) | Move a message to trash |
| [`POST /v1/email/messages/{messageId}/reply`](https://pozy.ai/docs/api/replyEmailMessage) | Reply to a message |
| [`GET /v1/email/folders`](https://pozy.ai/docs/api/listEmailFolders) | List mailbox folders |
| [`GET /v1/email/attachments/{attachmentId}`](https://pozy.ai/docs/api/downloadEmailAttachment) | Download an attachment |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`account.rate_limited`](https://pozy.ai/docs/webhooks/events#account-rate-limited) | Mailbox sending cooldown started. |
| [`message.received`](https://pozy.ai/docs/webhooks/events#message-received) | Message received. |
| [`message.sent`](https://pozy.ai/docs/webhooks/events#message-sent) | Message sent. |
