REST API reference

Everything an app or AI agent needs to run a mailbox: create inboxes on your domain, read mail, send, reply in-thread and get notified of new messages. Base URL https://havitomail.com/api/v1.

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:

Header
Authorization: Bearer hm_live_your_key_here

A 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:

json
{ "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.
No bulk mail, purchased lists or cold-email blasting. Accounts that send unsolicited mail are suspended.

Account

GET/me

Returns the account that owns the key, its plan and usage.

200 OK
{
  "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.

200 OK
{
  "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

200 OK
{
  "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

FieldTypeDescription
domain*stringA verified domain on your account, e.g. acme.com.
username*stringLocal part: 1–30 lowercase letters, digits, dots or hyphens. Becomes username@domain.
namestringDisplay name used in the From header. Defaults to the username.
Request
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"}'
201 Created
{
  "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" }
  }
}
The password is returned once. You don’t need it for the REST API — it is there so the same inbox can be opened in webmail or any IMAP/SMTP client.

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

FieldTypeDescription
folderstringFolder name. Default INBOX; others include Sent, Spam, Trash.
limitintegerMost 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.
unreadbooleantrue returns only messages not yet marked seen — the usual way to fetch work for an agent.
since_uidintegerOnly messages with a UID greater than this. Pass the next_since_uid from the previous response to poll incrementally.
200 OK
{
  "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

FieldTypeDescription
to*string | string[]One address, a comma-separated string, or an array.
subject*stringSubject line.
textstringPlain-text body. Send text, html or both.
htmlstringHTML body. A plain-text part is generated if you omit text.
ccstring | string[]Same formats as to.
bccstring | string[]Same formats as to.
attachmentsobject[]Up to 10 files, 10 MB total: { "filename": "quote.pdf", "content_type": "application/pdf", "content_base64": "JVBERi0…" }
in_reply_tostringMessage-ID being answered, e.g. <abc@example.com>. Prefer the reply endpoint, which fills this in for you.
referencesstringSpace-separated Message-ID chain for threading.
Request
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."
  }'
201 Created
{
  "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

FieldTypeDescription
textstringPlain-text reply body.
htmlstringHTML reply body.
reply_allbooleanAlso reply to the other original recipients. Default false.
ccstring | string[]Extra recipients to copy.
attachmentsobject[]Same format as when sending.
folderstringFolder of the original message. Default INBOX.
Request
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}

FieldTypeDescription
seenbooleanMark read / unread.
starredbooleanStar / unstar.
move_tostringMove to another folder, e.g. Archive or Trash.
folderstringCurrent folder of the message. Default INBOX.
bash
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

FieldTypeDescription
url*stringHTTPS endpoint that receives events.
inbox_idstringOnly fire for this inbox. Omit to receive events for every inbox on the account.
201 Created
{
  "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.js (fetch)
// 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 (requests)
# 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.