Botmail Send API
Send, reply and forward. Sends are queued (202) and classified before delivery; watch `message.status` events for the outcome. Pass an `Idempotency-Key` header (or `idempotency_key` in the body, which wins) to make retries safe.
Send, reply and forward. Sends are queued (202) and classified before delivery; watch `message.status` events for the outcome. Pass an `Idempotency-Key` header (or `idempotency_key` in the body, which wins) to make retries safe.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/botmail-pro:botmail-pro-send-api"
curl "https://apis.io/api/v1/apis?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: Botmail Send API
version: 1.0.0
summary: Gmail for AI agents.
description: 'Real email mailboxes for AI agents. Agents claim an address with
`POST /v1/claim`, a human claims it in the browser, then the same API key
reads, sends and manages mail.
**Errors** always look like `{"error": {"code", "message", "hint?", "details?"}}`.
`code` is stable and machine-readable; `hint` says what to do next.
429 responses carry `Retry-After` (seconds).
**IDs** are prefixed strings: `acct_`, `mbx_`, `thr_`, `msg_`, `key_`, `clm_`.
Times are RFC 3339 UTC.
**Scopes**: `read`, `send`, `manage`. Pending claim keys (`bm_pending_…`)
have `read` + `invite` until a human claims the mailbox, then they gain
full access in place. `send` is dropped while the account is not `active`.
Each operation lists its required scope.'
servers:
- url: https://botmail.pro
security:
- bearerAuth: []
- cookieAuth: []
tags:
- name: Send
description: 'Send, reply and forward. Sends are queued (202) and classified before
delivery; watch `message.status` events for the outcome. Pass an
`Idempotency-Key` header (or `idempotency_key` in the body, which wins)
to make retries safe.'
paths:
/v1/send:
post:
tags:
- Send
operationId: sendMessage
summary: Send a new message
description: 'Scope: send. Not for pending keys (use `POST /v1/claim/notify`).
Max 10 MB per message including base64 attachments. Errors:
400 mailbox_required / bad_from / no_recipients / invalid_recipient /
too_many_recipients / empty_body / bad_attachment / bad_idempotency_key,
403 not_claimed / sending_disabled / mailbox_inactive,
413 message_too_large, 422 recipients_suppressed, 429 rate_limited
(daily send allowance; see `GET /v1/account/usage`).'
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SendRequest'
responses:
'202':
$ref: '#/components/responses/SendAccepted'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'413':
$ref: '#/components/responses/TooLarge'
'422':
$ref: '#/components/responses/Unprocessable'
'429':
$ref: '#/components/responses/RateLimited'
/v1/messages/{id}/reply:
post:
tags:
- Send
operationId: replyToMessage
summary: Reply to a message
description: 'Scope: send. Not for pending keys. Sets threading headers, recipients
and subject from the original. 404 message_not_found, 422
automated_sender (replying to no-reply/bulk mail). Other errors as
`POST /v1/send`.'
parameters:
- $ref: '#/components/parameters/IdPath'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
text:
type: string
html:
type: string
reply_all:
type: boolean
default: false
attachments:
type: array
items:
$ref: '#/components/schemas/AttachmentIn'
idempotency_key:
type: string
maxLength: 200
responses:
'202':
$ref: '#/components/responses/SendAccepted'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'413':
$ref: '#/components/responses/TooLarge'
'422':
$ref: '#/components/responses/Unprocessable'
'429':
$ref: '#/components/responses/RateLimited'
/v1/messages/{id}/forward:
post:
tags:
- Send
operationId: forwardMessage
summary: Forward a message
description: 'Scope: send. Not for pending keys. The original is included inline.
400 no_recipients when `to` is empty. Other errors as `POST /v1/send`.'
parameters:
- $ref: '#/components/parameters/IdPath'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- to
additionalProperties: false
properties:
to:
$ref: '#/components/schemas/AddressList'
cc:
$ref: '#/components/schemas/AddressList'
text:
type: string
description: Note above the forwarded message.
idempotency_key:
type: string
maxLength: 200
responses:
'202':
$ref: '#/components/responses/SendAccepted'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'413':
$ref: '#/components/responses/TooLarge'
'422':
$ref: '#/components/responses/Unprocessable'
'429':
$ref: '#/components/responses/RateLimited'
/v1/drafts:
post:
tags:
- Send
operationId: createDraft
summary: Write a draft (not sent)
description: 'Scope: send. Saves an email without sending it and returns
`review_url` (https://botmail.pro/approve/): a page that
shows the email with Send and Discard buttons and needs no sign-in, so
an agent can hand it to its human. The token is only returned here.
`reply_to_message_id` drafts a reply (recipients and subject default to
the reply''s). Same validation as sending (400 invalid_recipient /
no_recipients / empty_body, 422 undeliverable_domain); 429
too_many_drafts above 200 open drafts.'
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
mailbox_id:
type: string
to:
$ref: '#/components/schemas/AddressList'
cc:
$ref: '#/components/schemas/AddressList'
bcc:
$ref: '#/components/schemas/AddressList'
subject:
type: string
text:
type: string
html:
type: string
reply_to_message_id:
type: string
responses:
'201':
description: The draft, with review_url.
content:
application/json:
schema:
$ref: '#/components/schemas/Draft'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'422':
$ref: '#/components/responses/Unprocessable'
'429':
$ref: '#/components/responses/RateLimited'
get:
tags:
- Send
operationId: listDrafts
summary: List drafts
description: 'Scope: read. Newest first, up to 100. `status` = draft (default), sent, discarded or all.'
parameters:
- name: status
in: query
schema:
type: string
enum:
- draft
- sent
- discarded
- all
responses:
'200':
description: Drafts.
content:
application/json:
schema:
type: object
properties:
drafts:
type: array
items:
$ref: '#/components/schemas/Draft'
/v1/drafts/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
get:
tags:
- Send
operationId: getDraft
summary: Get a draft
responses:
'200':
description: The draft.
content:
application/json:
schema:
$ref: '#/components/schemas/Draft'
'404':
$ref: '#/components/responses/NotFound'
patch:
tags:
- Send
operationId: updateDraft
summary: Edit an unsent draft
description: 'Scope: send. Omitted fields are kept. 409 draft_closed once sent or discarded.'
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
properties:
to:
$ref: '#/components/schemas/AddressList'
cc:
$ref: '#/components/schemas/AddressList'
bcc:
$ref: '#/components/schemas/AddressList'
subject:
type: string
text:
type: string
html:
type: string
responses:
'200':
description: The draft.
content:
application/json:
schema:
$ref: '#/components/schemas/Draft'
'409':
$ref: '#/components/responses/Conflict'
delete:
tags:
- Send
operationId: discardDraft
summary: Discard a draft
responses:
'200':
description: Discarded.
'409':
$ref: '#/components/responses/Conflict'
/v1/drafts/{id}/send:
parameters:
- name: id
in: path
required: true
schema:
type: string
post:
tags:
- Send
operationId: sendDraft
summary: Send a draft
description: 'Scope: send. Goes through the normal send pipeline once (409 draft_closed if already sent or discarded).'
responses:
'202':
$ref: '#/components/responses/SendAccepted'
'409':
$ref: '#/components/responses/Conflict'
'429':
$ref: '#/components/responses/RateLimited'
components:
responses:
Conflict:
description: Conflicts with current state.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
TooLarge:
description: Too large (`message_too_large` over 10 MB, `attachment_too_large` over 25 MB).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
BadRequest:
description: Invalid request (`bad_request` or a more specific code).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
SendAccepted:
description: Accepted and queued (or the original result for a repeated idempotency key).
content:
application/json:
schema:
$ref: '#/components/schemas/SendResult'
NotFound:
description: Not found or not accessible with this credential.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Unauthorized:
description: Missing or invalid credentials (`unauthorized`, `invalid_token`).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
RateLimited:
description: Too many requests (`rate_limited`, `invite_limit`). Wait `Retry-After` seconds.
headers:
Retry-After:
description: Seconds to wait.
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Forbidden:
description: 'The credential can''t do this (`forbidden`, `csrf`, `account_inactive`, …).
Missing scopes are listed in `details.required_scope`; pending keys get
a hint to wait for the claim.
'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Unprocessable:
description: Well-formed but rejected; see `code` and `hint`.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
parameters:
IdempotencyKey:
name: Idempotency-Key
in: header
description: 'Up to 200 chars. A retry with the same key returns the original
result instead of sending again. `idempotency_key` in the body wins.
'
schema:
type: string
maxLength: 200
IdPath:
name: id
in: path
required: true
schema:
type: string
schemas:
SendResult:
type: object
required:
- message_id
- thread_id
- status
properties:
message_id:
type: string
thread_id:
type: string
status:
type: string
enum:
- queued
- held
- blocked
- sent
hint:
type: string
Draft:
type: object
required:
- id
- mailbox_id
- from
- to
- subject
- status
- created_at
- updated_at
properties:
id:
type: string
description: drf_…
mailbox_id:
type: string
from:
type: string
to:
type: array
items:
type: string
cc:
type: array
items:
type: string
bcc:
type: array
items:
type: string
subject:
type: string
text:
type: string
html:
type: string
reply_to_message_id:
type: string
status:
type: string
enum:
- draft
- sent
- discarded
sent_message_id:
type: string
review_url:
type: string
format: uri
description: Only in the create response.
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
AddressList:
description: One address string (may be "a@x, b@y") or an array of addresses.
oneOf:
- type: string
- type: array
items:
type: string
AttachmentIn:
type: object
required:
- filename
- content_base64
additionalProperties: false
properties:
filename:
type: string
content_type:
type: string
content_base64:
type: string
contentEncoding: base64
description: Standard base64 with padding.
Error:
type: object
required:
- code
- message
properties:
code:
type: string
description: Stable machine-readable code, e.g. `not_found`, `name_taken`, `rate_limited`.
message:
type: string
hint:
type: string
description: What to do next.
details:
type: object
additionalProperties: true
ErrorResponse:
type: object
required:
- error
properties:
error:
$ref: '#/components/schemas/Error'
SendRequest:
type: object
required:
- to
additionalProperties: false
properties:
mailbox_id:
type: string
description: Required when the credential can use more than one mailbox (400 mailbox_required).
from:
type: string
description: One of the mailbox's addresses. Default the primary address.
to:
$ref: '#/components/schemas/AddressList'
cc:
$ref: '#/components/schemas/AddressList'
bcc:
$ref: '#/components/schemas/AddressList'
subject:
type: string
text:
type: string
html:
type: string
attachments:
type: array
items:
$ref: '#/components/schemas/AttachmentIn'
idempotency_key:
type: string
maxLength: 200
securitySchemes:
bearerAuth:
type: http
scheme: bearer
description: '`Authorization: Bearer <key>` with an API key (`bm_live_…`) or a
pending claim key (`bm_pending_…`). OAuth 2.1 access tokens
(`bm_oat_…`) are audience-bound to the MCP endpoint `/mcp` and are
rejected on `/v1`; MCP clients discover OAuth via
`/.well-known/oauth-protected-resource/mcp`.
'
cookieAuth:
type: apiKey
in: cookie
name: bm_session
description: 'Web session for signed-in humans. Non-GET requests must send a JSON
`Content-Type` or `X-Botmail-CSRF: 1` (403 csrf otherwise).
'