Authentication
Create an API key in webmail under Settings → API & Agents (account owners only). Keys look like hm_live_…, are shown once and stored hashed on our side — if you lose one, revoke it and create another. Send it on every request:
Authorization: Bearer hm_live_your_key_hereA key can act on every mailbox and domain the owning account has. Give each agent or environment its own key so you can revoke one without touching the others.
Errors
Errors use standard HTTP status codes and a JSON body with a stable machine-readable code and a human-readable message:
{ "error": { "code": "INBOX_NOT_FOUND", "message": "No inbox with id "8f2c…" on this account." } }400— invalid or missing fields.401— missing, invalid or revoked API key.402— the account’s trial or plan has expired; renew to restore access (incoming mail is still received).403— plan limit reached (PLAN_LIMIT), or deleting an inbox that was created in the dashboard (NOT_API_INBOX).409— the address already exists (ADDRESS_TAKEN).404— inbox, message or webhook not found.429— API rate limit or a sending limit was hit. Back off and retry later.
Limits
Limits exist to keep HavitoMail’s sending reputation clean for everyone — which is exactly what keeps your agents’ mail out of spam folders.
- API: 120 requests per minute per key.
- Recipients: up to 50 per message (to + cc + bcc).
- Trial: 30 recipients per hour and 100 per day.
- Pro: 100 recipients per hour and 500 per day per mailbox, up to 2,000 per day across the account.
- Pro plan: 20 mailboxes (people and agents together), 5 domains, 10 GB storage.
- Attachments: up to 10 files and 10 MB in total per message, sent base64-encoded.
Account
GET/me
Returns the account that owns the key, its plan and usage.
{
"account": { "id": "a1b2c3d4-…", "email": "founder@acme.com", "name": "Acme Inc" },
"plan": "pro",
"status": "active",
"plan_ends_at": "2027-10-10T00:00:00.000Z",
"limits": {
"inboxes": 20, "domains": 5, "storage_gb": 10,
"recipients_per_message": 50, "api_requests_per_minute": 120
},
"usage": { "inboxes": 4, "domains": 1 }
}plan is trial or pro; status is trial, active, grace or locked.
Domains
GET/domains
Lists the domains on the account. Inboxes can only be created on domains with verified: true — verification (MX record) is done once in webmail.
{
"domains": [
{ "id": "d9e8…", "domain": "acme.com", "verified": true, "active": true, "created_at": "2026-09-01T10:00:00.000Z" }
]
}Inboxes
An inbox is a real mailbox — agent@yourdomain.com — with its own storage, IMAP/SMTP login and webmail access.
List inboxes
GET/inboxes
{
"inboxes": [
{
"id": "8f2c1e0a-…",
"email": "support-agent@acme.com",
"name": "Acme Support Agent",
"domain": "acme.com",
"created_at": "2026-10-10T09:00:00.000Z",
"created_via": "api"
}
]
}Create an inbox
POST/inboxes
| Field | Type | Description |
|---|---|---|
| domain* | string | A verified domain on your account, e.g. acme.com. |
| username* | string | Local part: 1–30 lowercase letters, digits, dots or hyphens. Becomes username@domain. |
| name | string | Display name used in the From header. Defaults to the username. |
curl -X POST https://havitomail.com/api/v1/inboxes \
-H "Authorization: Bearer $HAVITO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "acme.com", "username": "support-agent", "name": "Acme Support Agent"}'{
"id": "8f2c1e0a-5b7d-4c11-9a3e-2d6f0b9c7e41",
"email": "support-agent@acme.com",
"name": "Acme Support Agent",
"domain": "acme.com",
"created_at": "2026-10-10T09:00:00.000Z",
"created_via": "api",
"credentials": {
"username": "support-agent@acme.com",
"password": "shown-only-once",
"imap": { "host": "mail.havitomail.com", "port": 993, "security": "SSL/TLS" },
"smtp": { "host": "mail.havitomail.com", "port": 465, "security": "SSL/TLS" }
}
}Get an inbox
GET/inboxes/{id}
Anywhere an inbox {id} appears in a path you can also use its email address, e.g. /inboxes/support-agent@acme.com/messages.
Delete an inbox
DELETE/inboxes/{id}
Permanently deletes the mailbox and its stored mail, and frees the slot on your plan. This cannot be undone. Only inboxes created through the API can be deleted through it — mailboxes created in the dashboard (your team’s) return 403 NOT_API_INBOX and must be removed there.
Messages
List messages
GET/inboxes/{id}/messages
| Field | Type | Description |
|---|---|---|
| folder | string | Folder name. Default INBOX; others include Sent, Spam, Trash. |
| limit | integer | Most recent N messages, newest first. Default 20, max 50. Bodies over 4,000 characters are cut (text_truncated: true) — read the message for the full text. |
| unread | boolean | true returns only messages not yet marked seen — the usual way to fetch work for an agent. |
| since_uid | integer | Only messages with a UID greater than this. Pass the next_since_uid from the previous response to poll incrementally. |
{
"inbox": { "id": "8f2c1e0a-…", "email": "support-agent@acme.com" },
"folder": "INBOX",
"total": 57,
"next_since_uid": 42,
"messages": [
{
"uid": 42,
"folder": "INBOX",
"message_id": "<CAF3x9k2@mail.example.com>",
"in_reply_to": null,
"references": [],
"from": { "name": "Sam Lee", "address": "sam@example.com" },
"to": [{ "name": "", "address": "support-agent@acme.com" }],
"cc": [],
"reply_to": [],
"subject": "Where is my refund?",
"date": "2026-10-10T09:14:03.000Z",
"seen": false,
"starred": false,
"answered": false,
"text": "Hi, I returned the jacket last week…",
"attachments": [{ "part_id": "2", "filename": "receipt.pdf", "content_type": "application/pdf", "size": 48213 }]
}
]
}Read a message
GET/inboxes/{id}/messages/{uid}?folder=INBOX
Returns one message with full plain-text and HTML bodies, cc, reply_to, in_reply_to and the attachment list. Reading does not mark the message as seen — do that explicitly with PATCH once your agent has handled it.
Download an attachment
GET/inboxes/{id}/messages/{uid}/attachments/{part_id}?folder=INBOX
Returns the raw file bytes (application/octet-stream; the original type is in the X-Content-Type header). Use the part_id from the message’s attachment list.
Send an email
POST/inboxes/{id}/messages
| Field | Type | Description |
|---|---|---|
| to* | string | string[] | One address, a comma-separated string, or an array. |
| subject* | string | Subject line. |
| text | string | Plain-text body. Send text, html or both. |
| html | string | HTML body. A plain-text part is generated if you omit text. |
| cc | string | string[] | Same formats as to. |
| bcc | string | string[] | Same formats as to. |
| attachments | object[] | Up to 10 files, 10 MB total: { "filename": "quote.pdf", "content_type": "application/pdf", "content_base64": "JVBERi0…" } |
| in_reply_to | string | Message-ID being answered, e.g. <abc@example.com>. Prefer the reply endpoint, which fills this in for you. |
| references | string | Space-separated Message-ID chain for threading. |
curl -X POST https://havitomail.com/api/v1/inboxes/$INBOX_ID/messages \
-H "Authorization: Bearer $HAVITO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "customer@example.com",
"subject": "Your order has shipped",
"text": "Hi Sam, your order #1042 left our warehouse today."
}'{
"sent": true,
"message_id": "<1791630115483.e4xo969enah@acme.com>",
"from": "support-agent@acme.com",
"to": ["sam@example.com"], "cc": [], "bcc": [],
"subject": "Your refund"
}Mail is sent from the inbox’s own address, DKIM-signed for your domain, and a copy is saved to its Sent folder.
Reply to a message
POST/inboxes/{id}/messages/{uid}/reply
| Field | Type | Description |
|---|---|---|
| text | string | Plain-text reply body. |
| html | string | HTML reply body. |
| reply_all | boolean | Also reply to the other original recipients. Default false. |
| cc | string | string[] | Extra recipients to copy. |
| attachments | object[] | Same format as when sending. |
| folder | string | Folder of the original message. Default INBOX. |
curl -X POST https://havitomail.com/api/v1/inboxes/$INBOX_ID/messages/42/reply \
-H "Authorization: Bearer $HAVITO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Thanks! Your refund was issued today."}'The reply goes to the original sender (or their Reply-To), gets a Re: subject and correct In-Reply-To/References headers, so it lands in the same thread in Gmail, Outlook and Apple Mail. The original is flagged as answered, so people looking at the same inbox in webmail can see the agent handled it.
Flags & move
PATCH/inboxes/{id}/messages/{uid}
| Field | Type | Description |
|---|---|---|
| seen | boolean | Mark read / unread. |
| starred | boolean | Star / unstar. |
| move_to | string | Move to another folder, e.g. Archive or Trash. |
| folder | string | Current folder of the message. Default INBOX. |
curl -X PATCH https://havitomail.com/api/v1/inboxes/$INBOX_ID/messages/42 \
-H "Authorization: Bearer $HAVITO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"seen": true, "move_to": "Archive"}'Webhooks
Instead of polling, register a URL and HavitoMail will POST a signed message.received event when new mail lands. Full payload and signature verification: Webhooks guide.
GET/webhooks
POST/webhooks
| Field | Type | Description |
|---|---|---|
| url* | string | HTTPS endpoint that receives events. |
| inbox_id | string | Only fire for this inbox. Omit to receive events for every inbox on the account. |
{
"id": "38e68acc-…",
"url": "https://agent.acme.com/havito-webhook",
"inbox_id": null,
"events": ["message.received"],
"active": true,
"created_at": "2026-10-10T09:00:00.000Z",
"last_delivery_at": null,
"last_status": null,
"last_error": null,
"secret": "whsec_…"
}The secret is returned only when the webhook is created — store it to verify signatures. Up to 10 webhooks per account; the URL must be public HTTPS.
DELETE/webhooks/{id}
Full examples
A minimal agent loop: fetch unread mail, draft a reply with your model, reply in-thread, mark as read.
// Node 18+ — poll an agent inbox and answer new mail
const API = "https://havitomail.com/api/v1";
const headers = {
Authorization: `Bearer ${process.env.HAVITO_API_KEY}`,
"Content-Type": "application/json",
};
const inboxId = process.env.INBOX_ID;
const res = await fetch(`${API}/inboxes/${inboxId}/messages?unread=true`, { headers });
const { messages } = await res.json();
for (const msg of messages) {
const answer = await myAgent.draftReply(msg.subject, msg.text); // your LLM call
await fetch(`${API}/inboxes/${inboxId}/messages/${msg.uid}/reply`, {
method: "POST",
headers,
body: JSON.stringify({ text: answer }), // threads under the original
});
await fetch(`${API}/inboxes/${inboxId}/messages/${msg.uid}`, {
method: "PATCH",
headers,
body: JSON.stringify({ seen: true }),
});
}# Python 3 — requests
import os, requests
API = "https://havitomail.com/api/v1"
H = {"Authorization": f"Bearer {os.environ['HAVITO_API_KEY']}"}
inbox = os.environ["INBOX_ID"]
msgs = requests.get(f"{API}/inboxes/{inbox}/messages",
params={"unread": "true", "limit": 20}, headers=H).json()["messages"]
for m in msgs:
answer = my_agent.draft_reply(m["subject"], m["text"]) # your LLM call
requests.post(f"{API}/inboxes/{inbox}/messages/{m['uid']}/reply",
json={"text": answer}, headers=H).raise_for_status()
requests.patch(f"{API}/inboxes/{inbox}/messages/{m['uid']}",
json={"seen": True}, headers=H)Running inside Claude or Cursor? The MCP server exposes the same actions as tools, no code needed.