mails-ai Billing API
Usage metering and the Stripe billing portal.
Usage metering and the Stripe billing portal.
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/mails-ai:mails-ai-billing-api"
curl "https://apis.io/api/v1/apis?limit=25"
Discovery needs no key. Ratings and market analysis are Pro.
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: Mails.ai Billing API
version: 0.2.0
description: 'The Mails.ai API gives AI agents a real email identity: send and receive
transactional email. Mail received within your plan''s limits is scanned for prompt injection and
reaches you as a typed event. With classification turned on (a paid-plan feature), new mail can also
be read for intent; a reply in a thread never is.
## Authentication
All endpoints take a Bearer API key:
```
Authorization: Bearer mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
Keys carry one or more scopes — `send`, `read`, `draft` (writes drafts, but can neither send nor
schedule one) and `manage` — and a mode: **live** (`mk_live_…`) or **test** (`mk_test_…`).
## Test mode
A `mk_test_` key exercises the entire API — real validation, persisted resources, real
`message.sent`/`*.received` events and webhooks — without sending real email or incurring cost. Build and
test your whole integration on a test key, then flip one character to `mk_live_` to go live.
`POST /v1/test/inbound` (test key only) lets you simulate inbound mail end-to-end.
## Idempotency
Send endpoints accept an `Idempotency-Key` header; a repeated key replays the stored response instead of
sending twice, with an `Idempotent-Replay: true` header.
## Pagination
List endpoints are cursor-paginated: pass `limit` (1–100) and the `next_cursor` from the previous page.
## Errors
Errors use a stable envelope: `{ "error": { "type", "code", "message", "param", "request_id" } }`. Every
response carries an `X-Request-Id` header for support correlation.'
contact:
name: Mails.ai Support
url: https://mails.ai
email: support@mails.ai
license:
name: Proprietary
identifier: LicenseRef-Proprietary
servers:
- url: https://api.mails.ai
description: Production
security:
- bearerAuth: []
tags:
- name: Billing
description: Usage metering and the Stripe billing portal.
paths:
/v1/billing/usage:
get:
tags:
- Billing
summary: Get current usage
operationId: getBillingUsage
description: '**Requires scope:** `read`. Returns metered usage for the current billing period plus the workspace''s tier caps. A key tied to one agent cannot read it (403 `insufficient_scope`): usage and costs are counted for the whole workspace, and `GET /v1/metrics` counts that agent''s mail.'
responses:
'200':
description: Current-period usage and caps.
content:
application/json:
schema:
$ref: '#/components/schemas/BillingUsage'
'401':
description: Missing or invalid API key.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: The API key lacks the required scope (`insufficient_scope`), or the token belongs to an app connected by sign-in and this operation is closed to connected apps (`connected_app_not_allowed`).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Resource not found in this workspace, or the agent a send names is archived (`agent_archived`).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
x-required-scope: read
/v1/billing/portal:
post:
tags:
- Billing
summary: Open billing portal or checkout
operationId: openBillingPortal
description: '**Requires scope:** `manage`. Returns a Stripe Billing Portal URL when already subscribed, otherwise a Checkout URL to subscribe. A workspace on an active paid plan with no Stripe subscription on file gets 409 `billing_state_inconsistent` instead of a Checkout URL, since subscribing again would bill it twice: contact support@mails.ai. Closed to apps connected by sign-in (403 `connected_app_not_allowed`): use the dashboard, an API key or the mails CLI.'
requestBody:
required: false
content:
application/json:
schema:
$ref: '#/components/schemas/BillingPortalRequest'
example:
return_url: https://app.acme.com/settings/billing
responses:
'201':
description: A redirect URL.
content:
application/json:
schema:
type: object
properties:
url:
type: string
format: uri
description: 'The page to open: a Stripe Billing Portal session when the workspace has a subscription, otherwise a Stripe Checkout session for `price_lookup_key`. Both lead back to `return_url`. When `mock` is true, it is `return_url` itself with mock query parameters.'
mock:
type: boolean
description: True when the server runs with Stripe mocked, so no Stripe session was created; false for a real session.
required:
- url
'400':
description: Invalid request — malformed JSON or a field failed validation. Also `feature_not_enabled` when this server has a feature switched off (custom domains before they are available).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: Missing or invalid API key.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: The API key lacks the required scope (`insufficient_scope`), or the token belongs to an app connected by sign-in and this operation is closed to connected apps (`connected_app_not_allowed`).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: Resource not found in this workspace, or the agent a send names is archived (`agent_archived`).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: 'Conflict — duplicate resource, an in-progress idempotent request, a draft or scheduled message that started sending while it was being changed, or a thread deleted for good while one of its messages is being sent (`duplicate_resource`); or an agent name more than one agent has (`agent_name_conflict`: use the agent''s id); or a status change on an archived agent, since archiving cannot be undone (`agent_archived`).'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
x-required-scope: manage
components:
schemas:
BillingUsage:
type: object
properties:
workspace_id:
type: string
description: Your workspace id (`wsk_…`).
tier:
type: string
description: 'Your plan: `free`, `pro`, `scale` or `metered`, or an older plan, `outbound` or `outbound_pro`. While a payment is past due, `caps` are Free''s whatever this says.'
tier_status:
type: string
description: '`active`, or `past_due` after a failed renewal payment: the workspace then has Free plan limits until the payment goes through. `canceled` and `paused` refuse every send, with 402 `subscription_canceled` and `payment_required`.'
period_start:
type: string
format: date-time
description: 'Start of the usage period: the first day of this month, 00:00 UTC. The counts in `usage` start over each month.'
period_end:
type: string
format: date-time
description: 'End of the usage period, exclusive: the first day of next month, 00:00 UTC.'
usage:
type: object
properties:
sends:
type: integer
description: 'Recipients sent to this period: each live message counts once per `to`, `cc` and `bcc` address, and test sends do not count. `caps.monthly_sends` limits it.'
parses:
type: integer
description: Received emails processed this period, which `caps.monthly_parses` limits. Mail kept or dropped over a limit counts in `inbound_skipped` instead.
inbound_skipped:
type: integer
description: 'Received emails not processed this period: kept unclassified because the monthly allowance was used up or the subscription is inactive, or dropped unstored in a flood past `hourly_inbound` or `daily_inbound`.'
webhook_deliveries:
type: integer
description: Webhook deliveries your endpoints accepted with a 2xx answer this period. Test deliveries are not counted.
sends_cost_usd:
type: number
description: Estimated cost of delivering this period's sends, at $0.0001 per recipient. It is not what your plan charges.
parses_cost_usd:
type: number
description: Estimated cost of classifying this period's received mail. Like `sends_cost_usd`, it is not what your plan charges.
required:
- sends
- parses
- webhook_deliveries
description: This period's counts.
caps:
type: object
properties:
monthly_sends:
type:
- integer
- 'null'
description: Recipients the workspace may send to in a month, each live message counted once per `to`, `cc` and `bcc` address; past it, a send gets 429 `monthly_limit_exceeded` (`free_tier_exceeded` on Free limits). Null when the plan has no monthly limit.
monthly_parses:
type:
- integer
- 'null'
description: Received emails processed in a month; past it, mail is still stored but not classified, and counts in `inbound_skipped`. Null when the plan has no monthly limit.
agents:
oneOf:
- type: integer
- type: string
enum:
- unlimited
description: How many agents the plan allows, or the string `unlimited`. Past it, `POST /v1/agents` gets 402 `resource_cap_exceeded`.
hourly:
type: integer
description: 'Recipients the workspace may send to in any rolling hour, counted like `monthly_sends`: once per `to`, `cc` and `bcc` address. Mail scheduled for later counts from when it was accepted; mail that was canceled or refused does not count. Past it, a send gets 429 `hourly_limit_exceeded`. Test sends do not count.'
daily:
type: integer
description: Recipients the workspace may send to in any rolling 24 hours, counted as `hourly` is. Past it, a send gets 429 `daily_limit_exceeded`. Test sends do not count.
hourly_inbound:
type: integer
description: Received emails stored in any rolling hour. Past it, more mail is dropped without being stored, and counts in `inbound_skipped`.
daily_inbound:
type: integer
description: Received emails stored in any rolling 24 hours. Past it, more mail is dropped without being stored, and counts in `inbound_skipped`.
required:
- monthly_sends
- monthly_parses
- agents
description: 'The limits of the plan the workspace is held to now: its own, or Free''s while a payment is past due.'
required:
- workspace_id
- tier
- usage
- caps
BillingPortalRequest:
type: object
properties:
return_url:
type: string
format: uri
description: 'Where the customer lands after leaving the billing portal, or after finishing or cancelling checkout: an absolute URL. Left out, it is the dashboard''s `/billing` page.'
price_lookup_key:
type: string
minLength: 1
maxLength: 64
description: 'The plan checkout subscribes to: `mailsai_pro_monthly` (the default), `mailsai_scale_monthly`, `mailsai_pro_yearly` or `mailsai_scale_yearly`. Used only when the workspace has no subscription yet; any other value is a 400 `invalid_param_value`.'
additionalProperties: false
Error:
type: object
properties:
error:
type: object
properties:
type:
type: string
enum:
- invalid_request_error
- authentication_error
- permission_error
- rate_limit_error
- resource_error
- payment_error
- abuse_error
- api_error
description: High-level error category.
code:
type: string
enum:
- invalid_field
- missing_field
- missing_param
- invalid_param_value
- method_not_allowed
- invalid_idempotency_key
- duplicate_resource
- slug_invalid
- slug_reserved
- slug_taken
- agent_name_conflict
- body_too_large
- missing_authorization
- missing_api_key
- invalid_api_key
- expired_api_key
- revoked_api_key
- invalid_access_token
- email_not_verified
- insufficient_scope
- connected_app_not_allowed
- sso_required
- workspace_mismatch
- agent_not_owned
- rate_limit_exceeded
- daily_limit_exceeded
- hourly_limit_exceeded
- monthly_limit_exceeded
- agent_not_found
- agent_paused
- agent_archived
- resource_not_found
- recipient_suppressed
- recipient_not_allowed
- domain_not_verified
- ses_role_not_assumable
- ses_role_external_id_not_required
- ses_permission_denied
- ses_setup_failed
- ses_message_rejected
- ses_events_not_confirmed
- payment_required
- payment_method_failed
- subscription_canceled
- billing_state_inconsistent
- free_tier_exceeded
- plan_limit_exceeded
- cold_email_prohibited
- agent_suspended
- new_workspace_fanout_exceeded
- resource_cap_exceeded
- workspace_cap_exceeded
- feature_not_enabled
- test_mode_required
- live_mode_required
- unknown_test_address
- workspace_not_approved
- spam_pattern_detected
- classifier_rejected
- high_injection_score
- complaint_threshold_exceeded
- bounce_threshold_exceeded
- internal_error
- internal_server_error
- upstream_error
- upstream_unavailable
- service_unavailable
description: Stable machine-readable error code.
message:
type: string
description: Human-readable explanation.
param:
type:
- string
- 'null'
description: The offending request field, when applicable.
request_id:
type: string
description: Echo of the X-Request-Id header for support correlation.
errors:
type: array
items:
$ref: '#/components/schemas/FieldError'
description: On a validation failure, EVERY offending field (not just the first). The top-level code/message/param mirror errors[0] for backward compatibility.
resets_at:
type:
- string
- 'null'
format: date-time
description: 'On every 429: when the limit lets the request through again (UTC). Null when waiting will not help, for a cap that only an upgrade or a deletion lifts.'
retry_after_seconds:
type:
- integer
- 'null'
description: 'On every 429: the seconds until `resets_at`, null when it is null. On any other error that says how long to wait, such as a 503 `upstream_unavailable`: the seconds its `Retry-After` header gives.'
upgrade_url:
type: string
format: uri
description: 'On every 402: the dashboard page where the plan can be changed, `https://app.mails.ai/billing`.'
required:
- type
- code
- message
description: 'The error, as every error response carries it: `type`, `code`, `message`, `param` and `request_id`, plus `errors` on a validation failure, `resets_at` and `retry_after_seconds` on a 429, `retry_after_seconds` on any other error with a `Retry-After`, and `upgrade_url` on a 402. A failed item in a batch send is the same error, and `retry_after_seconds` is how it says how long to wait, since an item has no headers of its own.'
required:
- error
FieldError:
type: object
properties:
code:
type: string
enum:
- invalid_field
- missing_field
- missing_param
- invalid_param_value
- method_not_allowed
- invalid_idempotency_key
- duplicate_resource
- slug_invalid
- slug_reserved
- slug_taken
- agent_name_conflict
- body_too_large
- missing_authorization
- missing_api_key
- invalid_api_key
- expired_api_key
- revoked_api_key
- invalid_access_token
- email_not_verified
- insufficient_scope
- connected_app_not_allowed
- sso_required
- workspace_mismatch
- agent_not_owned
- rate_limit_exceeded
- daily_limit_exceeded
- hourly_limit_exceeded
- monthly_limit_exceeded
- agent_not_found
- agent_paused
- agent_archived
- resource_not_found
- recipient_suppressed
- recipient_not_allowed
- domain_not_verified
- ses_role_not_assumable
- ses_role_external_id_not_required
- ses_permission_denied
- ses_setup_failed
- ses_message_rejected
- ses_events_not_confirmed
- payment_required
- payment_method_failed
- subscription_canceled
- billing_state_inconsistent
- free_tier_exceeded
- plan_limit_exceeded
- cold_email_prohibited
- agent_suspended
- new_workspace_fanout_exceeded
- resource_cap_exceeded
- workspace_cap_exceeded
- feature_not_enabled
- test_mode_required
- live_mode_required
- unknown_test_address
- workspace_not_approved
- spam_pattern_detected
- classifier_rejected
- high_injection_score
- complaint_threshold_exceeded
- bounce_threshold_exceeded
- internal_error
- internal_server_error
- upstream_error
- upstream_unavailable
- service_unavailable
description: Stable machine-readable code for this field.
message:
type: string
description: Human-readable explanation for this field.
param:
type:
- string
- 'null'
description: The offending request field (dot-path), or null.
required:
- code
- message
- param
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: mk_live_<32 hex>, mk_test_<32 hex>, or moa_<48 hex>
description: 'A Mails.ai API key, or an OAuth access token (`moa_…`, valid for 1 hour) from signing in through an app such as the CLI, Claude or ChatGPT. OAuth discovery: https://api.mails.ai/.well-known/oauth-authorization-server and https://api.mails.ai/.well-known/oauth-protected-resource. An access token''s OAuth scope maps onto these API scopes: `full_access` = `send` + `read` + `manage` (not `draft`: the draft routes take those three too), `send` = `send` + `read`, `read` = `read`. Scopes (`send`/`read`/`manage`/`draft`) are noted per operation via `x-required-scope`; when more than one scope opens an operation (the draft routes also take `draft`), `x-accepted-scopes` lists them all. `inbox` appears there too, on the triage routes (changing and trashing threads, labels, cancelling a scheduled send): only a signed-in dashboard session holds it (a workspace Member''s), never an API key or OAuth token, and `x-session-scopes` names it.'