The Mailbox API gives your app the same view of a mailbox that webmail has: the conversations in each folder, the messages in them, their attachments, and a way to reply. It is how a helpdesk, a CRM or an assistant reads and answers mail sent to addresses like [email protected].

Before you start

  • Create the mailbox in Ozi first, under Mailboxes. The API reads and sends mail for mailboxes that already exist; it does not create them.
  • Use an API key with Full access. Keys set to Sending only - the kind you hand to an app that just sends notifications - get 403 FORBIDDEN from every mailbox endpoint, because these endpoints read people’s mail.
  • A key restricted to one domain only sees that domain’s mailboxes. Conversations, messages and attachments outside the key’s team or domain return 404, exactly as if they did not exist.

Endpoints

Fields in mailbox responses use snake_case, such as next_cursor and last_message_at.

Using the SDK

How it behaves

  • Reading does not change anything. Getting a thread never marks it read. Mark it read yourself with PATCH /v1/threads/{id}.
  • Paging is stable. Threads come newest first. Pass next_cursor back as cursor; new mail arriving while you page lands on the first page and never shifts the pages after it.
  • Drafts are left out unless you ask for folder=drafts, and messages a mail app has marked for deletion never appear.
  • Long bodies are cut, not refused. A message’s text and html are trimmed in API responses when they are very large. A thread returns its latest 100 messages.
  • Replies go to the person who wrote last, and are filed in the mailbox’s Sent folder like a reply from webmail. They can be plain text or formatted (see below).
  • New messages start their own thread. POST /v1/mailboxes/{id}/messages sends to up to three addresses, gets the mailbox signature, is filed in Sent and returns the new thread_id. It never joins an existing conversation, even one with the same subject. A recipient on your suppression list refuses the whole message (recipient_suppressed) rather than sending to the others.
  • Attachments download as files. They always come back with Content-Disposition: attachment. Images embedded in a message body are not listed and cannot be downloaded through the API, and attachments over 25 MB were never stored, so they return 404 with a message saying so.

Formatted messages, pictures and scheduling

Replies and new messages take the same optional fields as webmail’s composer. Leave them all out and a message is plain text, sent at once, exactly as before.
  • html is a formatted body, up to 2 MB. It is cleaned on the server with the same rules webmail uses for what people write: scripts, forms, event handlers, <style> blocks, frames and unsafe links are removed before it is sent, so send ordinary formatting (paragraphs, bold, lists, links, tables, inline styles, pictures). When you leave out text, the plain-text copy is made from the HTML. The mailbox signature is added on the server: above an element with class="ozi_quote" when the body has one (wrap the quoted message you reply to in it), otherwise at the end. Do not add it yourself.
  • inline_images are pictures shown in the HTML, up to 10, each { "cid", "filename", "content_type", "content_base64" }. Show one with <img src="cid:logo">. cid is 1-64 letters, digits, ., _ or -, and unique in the message. content_type is image/png, image/jpeg, image/gif or image/webp, and the bytes must really be that type: they are checked. Each picture can be up to 1,350,000 bytes (the largest picture webmail’s composer holds). Every cid: the HTML shows must be supplied, and every picture supplied must be shown; otherwise the request is refused with 400 and nothing is sent. The filename’s extension is corrected to match the type.
  • draft_id names a draft of the same mailbox that this message replaces, such as the one a cancelled send became. Once the message is sent (or scheduled) the draft is deleted, as webmail does. It must be in that mailbox’s Drafts folder; anything else returns 404 and nothing is sent.
  • send_at (RFC 3339, such as 2026-10-09T09:00:00Z) holds the message until then, up to a year ahead. It waits in the mailbox’s Scheduled folder, where webmail shows it too, and the response has "scheduled": true, the scheduled_id and the send_at. A mailbox can have up to 100 messages waiting.

Undo send

GET /v1/mailboxes gives each mailbox’s undo_send_seconds, the delay its owner chose for Undo in webmail (0 when it is off). To offer the same undo, send with send_at that many seconds from now - send_at may be as soon as now - and if the person clicks Undo, cancel it with DELETE /v1/scheduled/{scheduled_id}. The cancel returns the message’s subject, recipients, HTML (with its pictures as data: URLs) and text, to put back in your composer, and keeps it as a draft in the mailbox’s Drafts folder under draft_id. Pass that draft_id when you send it again, so the draft does not stay behind in Drafts.

Changing a waiting message

GET /v1/mailboxes/{id}/scheduled lists what a mailbox has waiting, soonest first. POST /v1/scheduled/{id}/send-now sends one at once, and PATCH /v1/scheduled/{id} with { "send_at": ... } moves it (at least a minute ahead). Once a message has gone, or is on its way, these return 409 already_sent and change nothing.

Saved replies

Each mailbox keeps up to 100 saved replies, the same ones webmail’s composer offers: { "id", "name", "html", "updated_at" }. Names are 1-80 characters; the HTML, up to 100,000 characters, is cleaned on the server like a message body, and the cleaned form is what is returned.

Getting told about new mail

Rather than checking for new mail on a timer, add a webhook for these events: Both carry the same data, including the threadId to fetch with GET /v1/threads/{id}:
A webhook limited to certain domains only gets these events for mailboxes on those domains.

Errors