Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: OpenMail Messages API
description: Email infrastructure API for AI agents. Create inboxes, send and receive email, and get real-time notifications via webhooks.
version: 1.0.0
license:
name: Proprietary
servers:
- url: https://api.openmail.sh
security:
- bearerAuth: []
tags:
- name: Messages
description: Sending and listing messages
paths:
/v1/inboxes/{id}/send:
post:
operationId: sendEmail
summary: Send email
tags:
- Messages
description: 'Send an email from an inbox. Optionally pass an Idempotency-Key header to make retries safe: a repeat with the same key within 24 hours returns the original message instead of sending again. Without it the API generates a key and sends once. Use threadId to reply to an existing thread. When replying on a thread where your inbox was CC''d (deliveryRole cc), the original sender is automatically CC''d.
Every recipient this call can reach — `to`, `cc` (including any address auto-CC''d on a thread reply), `bcc`, and `replyTo` — is checked against the inbox''s outbound correspondent policy before anything is sent. A single denied recipient rejects the whole send with 403 `recipient_not_permitted`; there is no partial delivery.
`bcc` recipients are delivered and stored on the outbound message, but they are not listed on the copy To and Cc recipients receive. An address already in `to` or `cc` is not also blind-copied.'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- to
- body
properties:
to:
type: string
format: email
subject:
type: string
minLength: 1
maxLength: 998
description: 'Required for new threads. Optional when threadId is set — the thread subject is used automatically (with Re: prefix).'
body:
type: string
minLength: 1
maxLength: 100000
description: Email body. Plain text or HTML — HTML is auto-detected and rendered, with a plain-text fallback generated automatically.
bodyHtml:
type: string
maxLength: 500000
description: Provide only when the plain-text and HTML versions must differ. Omit for normal sends — put HTML in `body` instead.
threadId:
type: string
description: Thread ID for replies
includeQuote:
type: boolean
description: When replying with threadId, append a quoted copy of the previous message to the body. Defaults to true. Set false to send only your reply text.
replyTo:
type: string
format: email
description: 'Optional. Address that replies should be routed to (set as the `Reply-To` MIME header). Free plan: must be the address of an inbox you own in OpenMail. Pro plan or higher: any valid email address.'
cc:
type: array
items:
type: string
format: email
description: Optional CC recipients. Visible to every recipient.
unsubscribe:
type: boolean
description: 'Optional. Adds one-click `List-Unsubscribe` headers (RFC 8058), so Gmail, Yahoo and Apple Mail show a native unsubscribe button. Any `{{unsubscribe_url}}` in `body` or `bodyHtml` is replaced with the same link. A recipient who unsubscribes is suppressed for your whole organisation, and later sends to them, including replies, return 422 `recipient_suppressed` with type `unsub`. Can''t be combined with `cc` or `bcc`. A body containing `{{unsubscribe_url}}` without this flag is rejected.
On shared OpenMail domains this is automatic for any email to a single recipient (no `cc` or `bcc`) who is not another OpenMail inbox and has not written to you on the thread. It can''t be turned off there; use a custom domain for full control.'
bcc:
type: array
items:
type: string
format: email
description: Optional BCC recipients. Delivered, but omitted from the message To and Cc recipients can read. Use this for CRM logging addresses (for example HubSpot or Salesforce) that only record the email when they are in BCC. Checked against the inbox's outbound recipient policy, same as `to` and `cc`.
headers:
type: object
additionalProperties:
type: string
maxProperties: 20
description: 'Optional custom headers, as header name to string value, for example `{"List-Unsubscribe": "<https://example.com/u/123>"}` or `{"X-Campaign": "spring"}`. Up to 20 headers and 8 KB in total; each name plus value must fit in 996 characters. Values can''t contain line breaks. Headers that would change the sender, recipients, threading or authentication are rejected: From, Sender, To, Cc, Bcc, Subject, Date, Message-ID, In-Reply-To, References, Reply-To, Return-Path, MIME-Version, DKIM-Signature, Received, Authentication-Results, ARC-*, Feedback-ID, Delivered-To, X-Original-To, and anything starting with Content-, X-SES-, X-Mailgun- or X-OpenMail-. Use the matching field instead. Can''t set List-Unsubscribe when `unsubscribe` is true, or at all on a shared OpenMail domain.'
multipart/form-data:
schema:
type: object
required:
- to
- body
properties:
to:
type: string
format: email
subject:
type: string
minLength: 1
maxLength: 998
description: 'Required for new threads. Optional when threadId is set — the thread subject is used automatically (with Re: prefix).'
body:
type: string
minLength: 1
maxLength: 100000
description: Email body. Plain text or HTML — HTML is auto-detected and rendered, with a plain-text fallback generated automatically.
bodyHtml:
type: string
maxLength: 500000
description: Provide only when the plain-text and HTML versions must differ. Omit for normal sends — put HTML in `body` instead.
threadId:
type: string
description: Thread ID for replies
includeQuote:
type: boolean
description: When replying with threadId, append a quoted copy of the previous message to the body. Defaults to true. Set false to send only your reply text.
replyTo:
type: string
format: email
description: 'Optional. Address that replies should be routed to (set as the `Reply-To` MIME header). Free plan: must be the address of an inbox you own in OpenMail. Pro plan or higher: any valid email address.'
cc:
type: array
items:
type: string
format: email
description: Optional CC recipients. Repeat the `cc` field once per address; a single field may also hold a comma-separated list or a JSON-encoded array of addresses.
unsubscribe:
type: boolean
description: 'Optional. Adds one-click `List-Unsubscribe` headers (RFC 8058), so Gmail, Yahoo and Apple Mail show a native unsubscribe button. Any `{{unsubscribe_url}}` in `body` or `bodyHtml` is replaced with the same link. A recipient who unsubscribes is suppressed for your whole organisation, and later sends to them, including replies, return 422 `recipient_suppressed` with type `unsub`. Can''t be combined with `cc` or `bcc`. A body containing `{{unsubscribe_url}}` without this flag is rejected.
On shared OpenMail domains this is automatic for any email to a single recipient (no `cc` or `bcc`) who is not another OpenMail inbox and has not written to you on the thread. It can''t be turned off there; use a custom domain for full control.'
bcc:
type: array
items:
type: string
format: email
description: Optional BCC recipients. Repeat the `bcc` field once per address; a single field may also hold a comma-separated list or a JSON-encoded array of addresses. Not visible to To or Cc recipients.
headers:
type: string
description: Optional custom headers as a JSON-encoded object, for example `{"X-Campaign":"spring"}`. Same rules as the JSON `headers` field.
attachments:
type: array
items:
type: string
format: binary
description: File attachments (max 25 MB total per email)
parameters:
- schema:
type: string
in: path
name: id
required: true
- schema:
type: string
in: header
name: idempotency-key
required: false
description: 'Optional. Unique key (e.g. UUID) that makes retries safe: a repeat with the same key within 24 hours returns the original message instead of sending again. Generated server-side when omitted.'
responses:
'200':
description: Email sent (or idempotent replay)
content:
application/json:
schema:
description: Email sent (or idempotent replay)
type: object
additionalProperties: true
properties:
messageId:
type: string
threadId:
type: string
providerMessageId:
type: string
status:
type: string
enum:
- pending
- sent
- failed
'400':
description: Missing request body, invalid 'to'/'replyTo'/'cc'/'bcc' address, missing subject when required, `unsubscribe` combined with `cc`/`bcc`, or `{{unsubscribe_url}}` without `unsubscribe`, or invalid `headers` (`invalid_headers`)
content:
application/json:
schema:
description: Missing request body, invalid 'to'/'replyTo'/'cc'/'bcc' address, missing subject when required, `unsubscribe` combined with `cc`/`bcc`, or `{{unsubscribe_url}}` without `unsubscribe`, or invalid `headers` (`invalid_headers`)
allOf:
- $ref: '#/components/schemas/Error'
'402':
description: Sending is paused because this account's monthly spend limit was reached (`spend_cap_paused`)
content:
application/json:
schema:
description: Sending is paused because this account's monthly spend limit was reached (`spend_cap_paused`)
allOf:
- $ref: '#/components/schemas/Error'
'403':
description: A recipient is not permitted by the inbox's outbound correspondent policy (`recipient_not_permitted`, with the refused address in `recipient`). Also returned when a free-plan customer sets `replyTo` to an address that is not one of their own OpenMail inboxes (`feature_requires_paid_plan`), the account is banned (`account_banned`), or sending is suspended due to deliverability/abuse signals (`reputation_suspended`).
content:
application/json:
schema:
description: A recipient is not permitted by the inbox's outbound correspondent policy (`recipient_not_permitted`, with the refused address in `recipient`). Also returned when a free-plan customer sets `replyTo` to an address that is not one of their own OpenMail inboxes (`feature_requires_paid_plan`), the account is banned (`account_banned`), or sending is suspended due to deliverability/abuse signals (`reputation_suspended`).
allOf:
- $ref: '#/components/schemas/Error'
'404':
description: Inbox or thread not found
content:
application/json:
schema:
description: Inbox or thread not found
allOf:
- $ref: '#/components/schemas/Error'
'409':
description: The custom domain isn't ready to send yet (`domain_sending_pending`). Usually clears within minutes of verification; check the Retry-After response header.
content:
application/json:
schema:
description: The custom domain isn't ready to send yet (`domain_sending_pending`). Usually clears within minutes of verification; check the Retry-After response header.
allOf:
- $ref: '#/components/schemas/Error'
'422':
description: Content blocked (`unsafe_content`, `phishing_detected`), a recipient domain has no MX records (`no_mx_records`, with the failing domain in `domain`), recipient suppressed for bounce, complaint or unsubscribe (`recipient_suppressed`, with `type`), or account storage limit exceeded (`storage_limit_exceeded`)
content:
application/json:
schema:
description: Content blocked (`unsafe_content`, `phishing_detected`), a recipient domain has no MX records (`no_mx_records`, with the failing domain in `domain`), recipient suppressed for bounce, complaint or unsubscribe (`recipient_suppressed`, with `type`), or account storage limit exceeded (`storage_limit_exceeded`)
allOf:
- $ref: '#/components/schemas/Error'
'429':
description: Rate limit exceeded (global daily, per-inbox daily, per-inbox burst) or cold outreach limit (`cold_outreach_limit`, with `scope`, `limit`, `resetsAt`, and a `nextStep` sentence on what raises the cap — see `GET /v1/limits/cold`). Check the Retry-After response header.
content:
application/json:
schema:
description: Rate limit exceeded (global daily, per-inbox daily, per-inbox burst) or cold outreach limit (`cold_outreach_limit`, with `scope`, `limit`, `resetsAt`, and a `nextStep` sentence on what raises the cap — see `GET /v1/limits/cold`). Check the Retry-After response header.
allOf:
- $ref: '#/components/schemas/Error'
'503':
description: '`unsubscribe` was requested but one-click unsubscribe is unavailable (`unsubscribe_unavailable`). Retry, or send without it.'
content:
application/json:
schema:
description: '`unsubscribe` was requested but one-click unsubscribe is unavailable (`unsubscribe_unavailable`). Retry, or send without it.'
allOf:
- $ref: '#/components/schemas/Error'
/v1/inboxes/{id}/messages:
get:
operationId: listMessages
summary: List messages
tags:
- Messages
description: List all messages in an inbox, across every thread. Supports pagination for high-volume inboxes.
parameters:
- schema:
type: integer
default: 50
in: query
name: limit
required: false
- schema:
type: integer
default: 0
in: query
name: offset
required: false
- schema:
$ref: '#/components/schemas/MessageDirection'
in: query
name: direction
required: false
- schema:
type: string
in: path
name: id
required: true
responses:
'200':
description: List of messages
content:
application/json:
schema:
description: List of messages
type: object
additionalProperties: true
properties:
data:
type: array
items:
$ref: '#/components/schemas/Message'
total:
type: integer
'401':
description: Missing, invalid, or expired API key
content:
application/json:
schema:
description: Missing, invalid, or expired API key
allOf:
- $ref: '#/components/schemas/Error'
'404':
description: Inbox not found
content:
application/json:
schema:
description: Inbox not found
allOf:
- $ref: '#/components/schemas/Error'
/v1/messages/{id}/raw:
get:
operationId: getRawMessage
summary: Download raw message
tags:
- Messages
description: Returns the original RFC 822 bytes of an inbound message as `message/rfc822`, exactly as received.
parameters:
- schema:
type: string
in: path
name: id
required: true
responses:
'200':
description: Original message bytes
content:
message/rfc822:
schema:
type: string
format: binary
example: "Return-Path: <alice@example.com>\nReceived: from mail.example.com by inbound-smtp.eu-north-1.amazonaws.com\n for bot@yourdomain.com; Wed, 16 Sep 2026 18:22:58 +0000 (UTC)\nAuthentication-Results: amazonses.com; spf=pass; dkim=pass header.i=@example.com; dmarc=pass\nDKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=sel;\n h=from:to:subject:date:message-id:content-type; bh=y/Rmyv...; b=ZoOqF3...\nFrom: Alice <alice@example.com>\nTo: bot@yourdomain.com\nSubject: Q3 report\nDate: Wed, 16 Sep 2026 14:22:53 -0400\nMessage-ID: <CAJ=a=Hx...@mail.example.com>\nMIME-Version: 1.0\nContent-Type: multipart/mixed; boundary=\"b1\"\n\n--b1\nContent-Type: text/plain; charset=\"UTF-8\"\n\nReport attached.\n\n--b1\nContent-Type: application/pdf; name=\"report.pdf\"\nContent-Disposition: attachment; filename=\"report.pdf\"\nContent-Transfer-Encoding: base64\n\nJVBERi0xLjQKJ...\n--b1--\n"
'401':
description: Missing, invalid, or expired API key
content:
application/json:
schema:
description: Missing, invalid, or expired API key
allOf:
- $ref: '#/components/schemas/Error'
'404':
description: Message not found, or its original bytes weren't kept
content:
application/json:
schema:
description: Message not found, or its original bytes weren't kept
allOf:
- $ref: '#/components/schemas/Error'
/v1/messages/{id}:
delete:
operationId: deleteMessage
summary: Delete message
tags:
- Messages
description: Delete a message from its inbox.
parameters:
- schema:
type: string
in: path
name: id
required: true
responses:
'204':
description: Message deleted
'401':
description: Missing, invalid, or expired API key
content:
application/json:
schema:
description: Missing, invalid, or expired API key
allOf:
- $ref: '#/components/schemas/Error'
'403':
description: API key is inbox-scoped, or is not scoped to this message's inbox. Deleting mail needs an account-wide or pod-scoped key.
content:
application/json:
schema:
description: API key is inbox-scoped, or is not scoped to this message's inbox. Deleting mail needs an account-wide or pod-scoped key.
allOf:
- $ref: '#/components/schemas/Error'
'404':
description: Message not found
content:
application/json:
schema:
description: Message not found
allOf:
- $ref: '#/components/schemas/Error'
components:
schemas:
Error:
type: object
additionalProperties: true
properties:
error:
type: string
message:
type: string
Message:
type: object
additionalProperties: true
properties:
id:
type: string
threadId:
type: string
direction:
$ref: '#/components/schemas/MessageDirection'
fromAddr:
type: string
toAddr:
type: string
description: Your OpenMail inbox address for inbound messages. For outbound sends, the primary recipient.
headerTo:
type:
- string
- 'null'
description: Inbound only. The address the sender wrote to (comma-separated if multiple). When deliveryRole is cc, reply to this address — not fromAddr.
deliveryRole:
type:
- string
- 'null'
enum:
- to
- cc
- null
description: Inbound only. to if your inbox was the main recipient; cc if you were copied on the email.
cc:
type: array
items:
type: string
description: Email addresses copied on the message.
addressNames:
type:
- object
- 'null'
additionalProperties:
type: string
description: 'Inbound only. Display names the sender''s mail client put on From, To and Cc, keyed by lower-cased address, e.g. `{"alice@example.com": "Alice Smith"}`. Address fields stay bare so you can match on them. Null when the headers carried no names.'
bcc:
type: array
items:
type: string
description: Blind-copy recipients on outbound mail sent from this inbox. Empty on inbound. Not included in the copy delivered to To or Cc.
subject:
type: string
bodyText:
type: string
bodyHtml:
type: string
attachments:
type: array
items:
type: object
additionalProperties: true
properties:
filename:
type: string
contentType:
type: string
sizeBytes:
type: integer
url:
type: string
description: Signed URL for download
rawUrl:
type:
- string
- 'null'
description: URL of the original RFC 822 message (GET /v1/messages/{id}/raw). null when the bytes weren't kept.
status:
type: string
enum:
- pending
- sent
- received
- failed
createdAt:
type: string
format: date-time
MessageDirection:
type: string
enum:
- inbound
- outbound
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: API key