Email, calendar & files
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).
#Send an email
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" }
}'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);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.
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." }'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);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).
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, includingtext,htmlandattachments.includeHeaders=trueadds the headers;raw=truereturns 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:
{
"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 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 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 |
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 |
List email threads |
GET /v1/email/threads/{threadId} |
Get an email thread with its messages |
PATCH /v1/email/threads/{threadId} |
Update every message in a thread |
GET /v1/email/messages |
List email messages |
POST /v1/email/messages |
Send an email |
GET /v1/email/messages/{messageId} |
Get an email message |
PATCH /v1/email/messages/{messageId} |
Mark read or starred, or move a message |
DELETE /v1/email/messages/{messageId} |
Move a message to trash |
POST /v1/email/messages/{messageId}/reply |
Reply to a message |
GET /v1/email/folders |
List mailbox folders |
GET /v1/email/attachments/{attachmentId} |
Download an attachment |
#Webhook events
| Event | When it fires |
|---|---|
account.rate_limited |
Mailbox sending cooldown started. |
message.received |
Message received. |
message.sent |
Message sent. |