mails-ai Connected Apps API
The apps connected by signing in (the mails CLI, Claude, ChatGPT …): list and revoke them.
The apps connected by signing in (the mails CLI, Claude, ChatGPT …): list and revoke them.
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-connected-apps-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 Connected Apps 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: Connected Apps
description: 'The apps connected by signing in (the mails CLI, Claude, ChatGPT …): list and revoke them.'
paths:
/v1/oauth/grants:
get:
tags:
- Connected Apps
summary: List connected apps
operationId: listConnectedApps
description: '**Requires scope:** `manage`. The apps connected to this workspace by signing in, whoever in it connected them: the mails CLI and apps such as Claude, ChatGPT or Cursor. Only those that can still act are listed, newest first: one revoked, or whose tokens have all expired or been signed out, is not. Settings › Connected apps in the dashboard shows the same apps to everyone in the workspace. There, a member can revoke the apps they connected, and an owner or admin can revoke any of them. Closed to apps connected by sign-in (403 `connected_app_not_allowed`), and to a key tied to one agent (403): use an API key with `manage` that is not tied to an agent, or the mails CLI.'
parameters:
- name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 25
description: Page size (1–100).
- name: cursor
in: query
required: false
schema:
type: string
description: Opaque cursor from a previous page's `next_cursor`.
responses:
'200':
description: A page of connected apps.
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/ConnectedApp'
description: The items on this page, at most `limit` of them.
has_more:
type: boolean
description: 'True when more items follow this page: pass `next_cursor` as `cursor` to get them.'
next_cursor:
type: string
description: Pass as `cursor` to fetch the next page. Absent on the last page.
required:
- data
- has_more
'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'
'500':
description: Internal server error.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
x-required-scope: manage
/v1/oauth/grants/{id}:
delete:
tags:
- Connected Apps
summary: Revoke a connected app
operationId: revokeConnectedApp
description: '**Requires scope:** `manage`. Ends the sign-in, as Revoke in Settings › Connected apps does: every access and refresh token it was issued stops working at once, and the app has to be approved again to get back in. The API keys and webhook endpoints it created, and the webhook signing secrets it was shown, stay; the response counts them, so revoke and rotate those yourself. (Only when one of its refresh tokens is used twice, a sign of theft, does mails.ai remove them as well, and rotate those secrets again.) Idempotent: revoking an app already revoked returns its existing `revoked_at`. Closed to apps connected by sign-in (403 `connected_app_not_allowed`), and to a key tied to one agent (403): use an API key with `manage` that is not tied to an agent, or the mails CLI.'
parameters:
- name: id
in: path
required: true
schema:
type: string
description: The connected app's id (`oag_…`), from List connected apps.
responses:
'200':
description: The revoked app, with what it left behind.
content:
application/json:
schema:
$ref: '#/components/schemas/ConnectedAppRevoked'
'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: manage
components:
schemas:
ConnectedAppRevoked:
type: object
properties:
id:
type: string
description: 'The sign-in''s id (`oag_…`): the id in the path.'
client_id:
type: string
description: 'The app''s OAuth client id: the id it registered with, or the address of its client-ID document.'
client_name:
type:
- string
- 'null'
description: The name the app gave itself, at most 80 characters, or null when it gave none; `mails CLI` for the mails CLI.
first_party:
type: boolean
description: '`true` for the mails CLI, `false` for an app somebody connected by signing in.'
scope:
type: string
description: 'What the person had approved: `full_access`, `send` or `read`.'
scopes:
type: array
items:
type: string
enum:
- send
- read
- manage
description: 'What its tokens could do until the revoke, in the API key scopes: `full_access` = `send` + `read` + `manage`, `send` = `send` + `read`, `read` = `read`. Never more than the role of the person who approved it gave an app: a Member''s apps never hold `manage`, and a Billing member''s hold `read` only.'
created_at:
type: string
format: date-time
description: When the app was first approved.
approved_at:
type: string
format: date-time
description: When it was last approved before this revoke.
last_used_at:
type:
- string
- 'null'
format: date-time
description: When one of its tokens was last used, recorded at most once a minute. Null when none was used.
created_api_keys:
type: integer
description: API keys it created that still work after the revoke.
created_webhooks:
type: integer
description: Webhook endpoints it created, still in place after the revoke.
signing_secrets_known:
type: integer
description: Other webhook endpoints still signing with a secret it was shown.
revoked_at:
type: string
format: date-time
description: When the sign-in was ended and its tokens stopped working. For one that was already revoked, the time it ended then.
required:
- id
- client_id
- client_name
- first_party
- scope
- scopes
- created_at
- approved_at
- last_used_at
- created_api_keys
- created_webhooks
- signing_secrets_known
- revoked_at
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
ConnectedApp:
type: object
properties:
id:
type: string
description: The sign-in's id (`oag_…`). Approving the same app again in the same workspace keeps it.
client_id:
type: string
description: 'The app''s OAuth client id: the id it registered with, or the address of its client-ID document. The mails CLI''s is `https://api.mails.ai/oauth/clients/cli`.'
client_name:
type:
- string
- 'null'
description: The name the app gave itself; `mails CLI` for the mails CLI.
first_party:
type: boolean
description: '`true` for the mails CLI, `false` for an app somebody connected by signing in (Claude, ChatGPT, Cursor …).'
scope:
type: string
description: 'What the person approved: `full_access`, `send` or `read`.'
scopes:
type: array
items:
type: string
enum:
- send
- read
- manage
description: 'What its tokens may do, in the API key scopes: `full_access` = `send` + `read` + `manage`, `send` = `send` + `read`, `read` = `read`. Never more than the role of the person who approved it gives an app now: a Member''s apps never hold `manage`, and a Billing member''s hold `read` only.'
created_at:
type: string
format: date-time
description: When it was first approved.
approved_at:
type: string
format: date-time
description: When it was last approved.
last_used_at:
type:
- string
- 'null'
format: date-time
description: When one of its tokens was last used.
created_api_keys:
type: integer
description: 'API keys it created that still work (only the mails CLI, and keys it made, can create any). Revoking the app leaves them working: revoke them under API keys.'
created_webhooks:
type: integer
description: Webhook endpoints it created (only the mails CLI, and keys it made, can create any). Revoking the app leaves them in place.
signing_secrets_known:
type: integer
description: 'Other webhook endpoints still signing with a secret it was shown. Revoking the app leaves those secrets in use: rotate them (`POST /v1/webhooks/{id}/rotate-secret`).'
required:
- id
- client_id
- client_name
- first_party
- scope
- scopes
- created_at
- approved_at
- last_used_at
- created_api_keys
- created_webhooks
- signing_secrets_known
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.'