mails-ai Suppression API
Suppression lookups and the consent allowlist.
Suppression lookups and the consent allowlist.
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-suppression-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 Suppression 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: Suppression
description: Suppression lookups and the consent allowlist.
paths:
/v1/suppression:
get:
tags:
- Suppression
summary: List allowlist or look up an address
operationId: listSuppression
description: '**Requires scope:** `read`. Three answers from one route. With `?address=`, the suppression status of that address. With `?view=suppressed`, which of your own recent recipients are suppressed for you, and why (up to 500, in one page; addresses are stored hashed, so the list itself cannot be browsed). Without either, the workspace allowlist (paginated).'
parameters:
- name: address
in: query
required: false
schema:
type: string
format: email
description: Look up suppression status for this address instead of listing the allowlist.
- name: view
in: query
required: false
schema:
type: string
enum:
- suppressed
description: '`suppressed`: list which of your own recent recipients are suppressed for you, and why, instead of the allowlist.'
- 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 suppression lookup (when `address` is set), your suppressed recipients (when `view=suppressed`), or a page of allowlist entries.
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/SuppressionLookup'
- type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/SuppressedRecipient'
description: Your suppressed recipients, newest suppression first, at most 500 rows. Every `to`, `cc` and `bcc` address of your 2,000 most recent sends is checked (for a key tied to one agent, that agent's).
has_more:
type: boolean
description: '`true` when more of those recipients are suppressed than the 500 returned. This view takes no `limit` or `cursor`, so check any other address with `?address=`.'
next_cursor:
type: string
description: Pass as `cursor` to fetch the next page. Absent on the last page.
required:
- data
- has_more
- type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/SuppressionEntry'
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: read
/v1/suppression/allow:
post:
tags:
- Suppression
summary: Add an allowlist entry
operationId: allowSuppression
description: '**Requires scope:** `manage`. Attests that a recipient consents, overriding non-hard-bounce suppression for that address.'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AllowSuppressionRequest'
example:
address: jordan@example.com
attestation: 'Customer reconfirmed opt-in via account settings on 2026-06-20 (support ticket #4821).'
responses:
'201':
description: The created allowlist entry.
content:
application/json:
schema:
$ref: '#/components/schemas/SuppressionEntry'
'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/suppression/allow/{id}:
delete:
tags:
- Suppression
summary: Revoke an allowlist entry
operationId: revokeSuppressionAllow
description: '**Requires scope:** `manage`.'
parameters:
- name: id
in: path
required: true
schema:
type: string
description: Allowlist entry id (`sup_…`).
responses:
'200':
description: The revoked entry.
content:
application/json:
schema:
type: object
properties:
id:
type: string
description: 'Id of the revoked allowlist entry (`sup_…`): the id in the path.'
revoked_at:
type: string
format: date-time
description: 'When the entry was revoked: the time of this request. Revoking an entry that was already revoked sets a new time.'
required:
- id
- revoked_at
'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:
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
SuppressionEntry:
type: object
properties:
id:
type: string
description: Allowlist entry id (`sup_…`); revoke it with `DELETE /v1/suppression/allow/{id}`.
address:
type: string
format: email
description: 'The address the entry covers. The create response echoes it as sent; the list shows it as matched: lowercase, without a `+tag`, and for Gmail without dots before the `@`.'
attestation:
type: string
description: Your statement that the recipient agreed to receive your mail, as given when the entry was added.
revoked_at:
type: string
format: date-time
description: When the entry was revoked. Absent while it is active; a revoked entry stays in the list but lifts nothing.
created_at:
type: string
format: date-time
description: When the entry was added (UTC).
required:
- id
- address
- attestation
- created_at
AllowSuppressionRequest:
type: object
properties:
address:
type: string
format: email
description: The recipient to send to again despite a complaint or unsubscribe; a hard bounce is not lifted. Matched in lowercase, without any `+tag` and, for gmail.com and googlemail.com, without dots before the `@`, so one entry covers each such spelling.
attestation:
type: string
minLength: 20
maxLength: 2000
description: Your statement that the recipient agreed to receive your mail, 20 to 2,000 characters. It is stored with the entry and returned with it.
required:
- address
- attestation
additionalProperties: false
SuppressionLookup:
type: object
properties:
suppressed:
type: boolean
description: 'True when your sends to the address are refused: it hard-bounced or drew a spam complaint for any mails.ai sender, or unsubscribed from your mail. An allowlist entry lifts all but a hard bounce, and then this is false with `allowed` set.'
type:
type: string
enum:
- hard_bounce
- complaint
- unsubscribe
description: Present only when suppressed.
scope:
type: string
description: '`global` (hard bounces and complaints, for every sender) or `workspace:<your id>` (an unsubscribe from your mail). Present only when suppressed.'
since:
type: string
format: date-time
description: When suppression began. Present only when suppressed.
allowed:
type: object
properties:
id:
type: string
description: Id of the allowlist entry (`sup_…`) that lifts the suppression.
attestation:
type: string
description: The consent statement given when the entry was added.
created_at:
type: string
description: When the entry was added (ISO 8601, UTC).
required:
- id
description: Present when an allowlist attestation overrides the suppression.
required:
- suppressed
SuppressedRecipient:
type: object
properties:
address:
type: string
format: email
description: One of this workspace's recent recipients.
type:
type: string
enum:
- hard_bounce
- complaint
- unsubscribe
description: Why the address is suppressed. An address suppressed for more than one reason has a row for each; a reason an allowlist entry lifts is left out.
scope:
type: string
description: '`global`, or `workspace:<your id>` for an unsubscribe from your mail.'
detail:
type: string
description: What caused it, when recorded.
suppressed_at:
type: string
format: date-time
description: When the suppression was recorded (UTC).
required:
- address
- type
- scope
- suppressed_at
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.'