Endpoints
POST /api/v1/agents/register
Zero-signup: mint an API key + inbox for an AI agent (no auth, no email).
Request body
application/json
{"name": "optional label, max 60 chars"}
Responses
| Status | Meaning |
201 |
api_key (shown once), key_prefix, plan, and the agent's first inbox. |
429 |
Per-IP or global agent-registration rate limit exceeded. |
POST /api/v1/verify
Verify an email address's deliverability (syntax + MX + heuristics, never SMTP-probes).
Request body
Responses
| Status | Meaning |
200 |
deliverable (string enum: deliverable | risky | undeliverable | unknown), syntax_valid, has_mx, is_disposable, is_role, reason. |
401 |
Missing or invalid API key. |
403 |
Account is not permitted to use the API. |
422 |
Missing or malformed email. |
429 |
Rate limit exceeded (per-key, per-account, or global verify capacity). |
POST /api/v1/verify/batch
Bulk email verification (up to 100 unique rows safe / 25 full) — same charged W7 path per row, not a rate-limit bypass.
Request body
application/json
{"emails": ["[email protected]", ...]} or {"text": "paste blob"} or multipart file=<.csv|.txt> — exactly one, plus optional "mode": "safe"|"full"
Responses
| Status | Meaning |
200 |
data.mode, data.results (array of the SAME per-row shape as POST /verify, in input order), data.summary (total/deliverable/risky/undeliverable/unknown). |
401 |
Missing or invalid API key. |
403 |
Account is not permitted, full-mode SMTP verify disabled (smtp_verify_disabled), or the plan lacks the full-mode entitlement (verify_full_entitlement_required). |
422 |
validation_error (both/neither emails+text, or zero unique rows), batch_too_large (over the mode's cap: 100 safe / 25 full), unsupported_file_type, malformed_file, or file_too_large (64KB cap). |
429 |
Rate limit exceeded — the batch's whole N-sized charge did not fit the remaining per-key/per-account /verify budget; nothing was consumed. |
GET /api/v1/inboxes/{id}/stream
Server-Sent Events push of new messages (EventSource-compatible alternative to /wait).
Parameters
| Param | Type | Default | Constraint |
id |
string (path) |
required |
inbox public id |
since |
string |
request start time |
ISO-8601 datetime | message public_id | unix timestamp; messages strictly after this match |
require |
string |
any |
otp | link | any |
min_confidence |
number |
0.0 |
OTP must have otp_confidence ≥ this; clamped to [0, 1]; ignored for link |
Responses
| Status | Meaning |
200 |
text/event-stream: `message` events (Message JSON), periodic heartbeats, a final `bye` before the connection is recycled (~25s) for EventSource auto-reconnect. |
401 |
Missing or invalid API key. |
403 |
Account is not permitted to use the API. |
404 |
Unknown or not owned. |
429 |
too_many_wait_requests (per-account cap) or wait_capacity (global cap). |
GET /api/v1/inboxes
List inboxes.
Parameters
| Param | Type | Default | Constraint |
limit |
integer |
25 |
1–100 |
cursor |
string |
— |
opaque, signed, owner- and filter-bound |
Responses
| Status | Meaning |
200 |
A page of inboxes. |
401 |
Missing or invalid API key. |
403 |
Account is not permitted to use the API. |
422 |
Invalid parameter. |
429 |
Rate limit exceeded. |
Successful responses wrap the payload as {"data": <InboxListResponse>} (plus pagination on list endpoints). Field definitions live in /docs/openapi.yaml.
POST /api/v1/inboxes
Create an inbox.
Request body
application/json
{"label": "optional label, max 120 chars", "routing_key": "optional custom slug, 3-64 chars, a-z/A-Z/0-9/_/- (normalized to lowercase)", "public": "optional bool, default false — opt-in anonymous read (shared domain only)", "metadata": "optional JSON object, <=4096 bytes compact UTF-8, <=50 top-level keys"}
Responses
| Status | Meaning |
201 |
Inbox created; Location header points at the new inbox. |
401 |
Missing or invalid API key. |
403 |
Account is not permitted to use the API. |
409 |
plan_limit_reached (plan inbox limit), slug_taken (routing_key already claimed), or conflict (allocation retries exhausted). |
415 |
Request media type not supported. |
422 |
Request validation failed, or public_not_allowed_on_custom_domain / metadata_must_be_object / metadata_too_large. |
429 |
Rate limit exceeded. |
Successful responses wrap the payload as {"data": <Inbox>} (plus pagination on list endpoints). Field definitions live in /docs/openapi.yaml.
GET /api/v1/inboxes/{id}
Get an inbox.
Parameters
| Param | Type | Default | Constraint |
id |
string (path) |
required |
inbox public id, e.g. inbox_... |
Responses
| Status | Meaning |
200 |
The inbox, plus message_count_month and webhook_configured. |
401 |
Missing or invalid API key. |
403 |
Account is not permitted to use the API. |
404 |
Unknown or not owned. |
429 |
Rate limit exceeded. |
Successful responses wrap the payload as {"data": <InboxDetail>} (plus pagination on list endpoints). Field definitions live in /docs/openapi.yaml.
DELETE /api/v1/inboxes/{id}
Delete an inbox.
Parameters
| Param | Type | Default | Constraint |
id |
string (path) |
required |
inbox public id, e.g. inbox_... |
Responses
| Status | Meaning |
204 |
Inbox soft-deleted (disabled). A second delete returns 404. |
401 |
Missing or invalid API key. |
403 |
Account is not permitted to use the API. |
404 |
Unknown or not owned. |
429 |
Rate limit exceeded. |
GET /api/v1/inboxes/{id}/messages
List messages in an inbox.
Parameters
| Param | Type | Default | Constraint |
id |
string (path) |
required |
inbox public id |
limit |
integer |
25 |
1–100 |
cursor |
string |
— |
opaque, signed, owner- and filter-bound |
has_otp |
boolean |
— |
keep only messages with a non-empty OTP |
subject_contains |
string |
— |
case-insensitive substring, capped at 200 chars |
from |
string |
— |
case-insensitive substring on the From header |
Responses
| Status | Meaning |
200 |
A page of message summaries. |
401 |
Missing or invalid API key. |
403 |
Account is not permitted to use the API. |
404 |
Unknown or not owned. |
422 |
Invalid parameter. |
429 |
Rate limit exceeded. |
Successful responses wrap the payload as {"data": <MessageSummaryListResponse>} (plus pagination on list endpoints). Field definitions live in /docs/openapi.yaml.
GET /api/v1/inboxes/{id}/messages/latest
Get the latest message in an inbox.
Parameters
| Param | Type | Default | Constraint |
id |
string (path) |
required |
inbox public id |
Responses
| Status | Meaning |
200 |
The newest message (full representation). |
401 |
Missing or invalid API key. |
403 |
Account is not permitted to use the API. |
404 |
Unknown inbox or no messages yet. |
429 |
Rate limit exceeded. |
Successful responses wrap the payload as {"data": <Message>} (plus pagination on list endpoints). Field definitions live in /docs/openapi.yaml.
GET /api/v1/inboxes/{id}/messages/wait
Wait for an OTP or magic link.
Parameters
| Param | Type | Default | Constraint |
id |
string (path) |
required |
inbox public id |
timeout |
number (seconds) |
20 |
clamped to [1, 25] |
since |
string |
request start time |
ISO-8601 datetime | message public_id | unix timestamp; messages strictly after this match |
require |
string |
otp |
otp | link | any |
min_confidence |
number |
0.0 |
OTP must have otp_confidence ≥ this; clamped to [0, 1]; ignored for link |
Responses
| Status | Meaning |
200 |
A matching parsed message arrived. |
204 |
Waited, nothing matched yet — call again. Success, not an error. |
401 |
Missing or invalid API key. |
403 |
Account is not permitted to use the API. |
404 |
Unknown or not owned. |
429 |
too_many_wait_requests (per-account cap) or wait_capacity (global cap). |
Successful responses wrap the payload as {"data": <Message>} (plus pagination on list endpoints). Field definitions live in /docs/openapi.yaml.
GET /api/v1/messages/{id}
Get a message.
Parameters
| Param | Type | Default | Constraint |
id |
string (path) |
required |
message public id, e.g. msg_... |
Responses
| Status | Meaning |
200 |
The message (full representation). |
401 |
Missing or invalid API key. |
403 |
Account is not permitted to use the API. |
404 |
Unknown or not owned. |
429 |
Rate limit exceeded. |
Successful responses wrap the payload as {"data": <Message>} (plus pagination on list endpoints). Field definitions live in /docs/openapi.yaml.
GET /api/v1/public/emails/{email}/messages
Anonymous read of a public inbox's recent messages (no auth, opt-in only).
Parameters
| Param | Type | Default | Constraint |
email |
string (path) |
required |
a shared-domain address, e.g. [email protected] |
limit |
integer |
20 |
1–20 |
Responses
| Status | Meaning |
200 |
Up to `limit` newest messages (PublicMessageSerializer — no otp_candidates, metadata, text, or html). |
404 |
Address malformed, unknown, private (is_public=False, the default), disabled, deleted, or on a custom domain — all identical, no oracle. |
429 |
Per-IP rate limit exceeded (30/minute). |
POST /api/v1/domains/{id}/sending_setup
Provision a fresh per-domain DKIM keypair for outbound sending.
Parameters
| Param | Type | Default | Constraint |
id |
string (path) |
required |
domain public id |
Responses
| Status | Meaning |
200 |
Domain with dkim_status=pending_dns, dkim_selector, and dkim_public_key_txt (publish this TXT record). |
401 |
Missing or invalid API key. |
403 |
Account is not permitted to use the API, or the API key was not minted by the tenant owner. |
404 |
Unknown or not owned, or ENABLE_OUTBOUND is off. |
409 |
domain_not_verified — the domain must be `verified` before DKIM setup. |
429 |
Rate limit exceeded (6/hour/domain). |
Successful responses wrap the payload as {"data": <Domain>} (plus pagination on list endpoints). Field definitions live in /docs/openapi.yaml.
POST /api/v1/outbound_emails
Send an email from a verified, DKIM-verified custom domain (Business plan only).
Request body
application/json
{"from_address": "[email protected]", "to": ["[email protected]"], "cc": [], "bcc": [], "reply_to": "", "subject": "...", "body_text": "...", "body_html": "..."}
Responses
| Status | Meaning |
201 |
Email queued (status=queued); sent asynchronously by the drain. |
401 |
Missing or invalid API key. |
403 |
Account is not permitted, is not Business-tier / is demo-or-agent, or sending_domain_not_eligible (from_address's domain is not owned+verified+DKIM-verified — no oracle for which of the four causes). |
404 |
ENABLE_OUTBOUND is off. |
422 |
Malformed field, CRLF/control characters in a user field, too many recipients (>10), or one or more recipients are suppressed. |
429 |
outbound_rate_limited (velocity/queue-depth) or the per-key/global API rate limit. |
Successful responses wrap the payload as {"data": <OutboundEmail>} (plus pagination on list endpoints). Field definitions live in /docs/openapi.yaml.
GET /api/v1/outbound_emails/{id}
Get the status of a sent/queued outbound email.
Parameters
| Param | Type | Default | Constraint |
id |
string (path) |
required |
outbound email public id |
Responses
| Status | Meaning |
200 |
The outbound email (status: queued | sending | sent | failed | rejected | bounced). |
401 |
Missing or invalid API key. |
403 |
Account is not permitted to use the API. |
404 |
Unknown or not owned, or ENABLE_OUTBOUND is off. |
Successful responses wrap the payload as {"data": <OutboundEmail>} (plus pagination on list endpoints). Field definitions live in /docs/openapi.yaml.
GET /api/v1/outbound_allowlist
List approved recipients for outbound send (Business plan only).
Responses
| Status | Meaning |
200 |
Paginated list of the caller's approved recipients. |
401 |
Missing or invalid API key. |
403 |
Account is not permitted, the plan does not include the allowlist (outbound_allowlist_entitlement_required), or the API key was not minted by the tenant owner. |
404 |
ENABLE_OUTBOUND is off. |
Successful responses wrap the payload as {"data": <ApprovedRecipient>} (plus pagination on list endpoints). Field definitions live in /docs/openapi.yaml.
POST /api/v1/outbound_allowlist
Approve a recipient address for outbound send (Business plan only).
Request body
Responses
| Status | Meaning |
201 |
Recipient approved. |
401 |
Missing or invalid API key. |
403 |
Account is not permitted, the plan does not include the allowlist, or the API key was not minted by the tenant owner. |
404 |
ENABLE_OUTBOUND is off. |
409 |
approved_recipient_exists — that address is already approved for this account. |
422 |
Malformed email, or the allowlist entry cap has been reached. |
429 |
Too many allowlist changes. |
Successful responses wrap the payload as {"data": <ApprovedRecipient>} (plus pagination on list endpoints). Field definitions live in /docs/openapi.yaml.
DELETE /api/v1/outbound_allowlist/{id}
Revoke an approved recipient (Business plan only).
Parameters
| Param | Type | Default | Constraint |
id |
string (path) |
required |
approved-recipient public id |
Responses
| Status | Meaning |
204 |
Recipient revoked. |
401 |
Missing or invalid API key. |
403 |
Account is not permitted, the plan does not include the allowlist, or the API key was not minted by the tenant owner. |
404 |
Unknown or not owned, or ENABLE_OUTBOUND is off. |
POST /api/v1/outbound_allowlist/mode
Enable/disable the approved-recipients allowlist enforcement (Business plan only).
Request body
application/json
{"enabled": true}
Responses
| Status | Meaning |
200 |
{"data": {"enabled": true|false}} — reflects the new Account.outbound_allowlist_enabled state. |
401 |
Missing or invalid API key. |
403 |
Account is not permitted, the plan does not include the allowlist, or the API key was not minted by the tenant owner. |
404 |
ENABLE_OUTBOUND is off. |
POST /api/v1/inboxes/{inbox_id}/messages/{message_id}/reply
Reply to an inbound message from its inbox's verified, DKIM-verified custom domain (Business plan only).
Parameters
| Param | Type | Default | Constraint |
inbox_id |
string (path) |
required |
inbox public id |
message_id |
string (path) |
required |
message public id |
Request body
application/json
{"body_text": "...", "body_html": "...", "cc": [], "reply_all": false}
Responses
| Status | Meaning |
201 |
Reply queued (status=queued), threaded via In-Reply-To/References when the original Message-ID is well-formed. |
401 |
Missing or invalid API key. |
403 |
Account is not permitted, is not Business-tier / is demo-or-agent, or the inbox's domain is not eligible to send from (sending_domain_not_eligible — same as W5). |
404 |
Unknown/foreign inbox or message (uniform, no oracle), or ENABLE_OUTBOUND is off. |
422 |
reply_target_invalid (no usable Reply-To/From/envelope address), too many recipients, or one or more recipients are suppressed/not on the allowlist. |
429 |
outbound_rate_limited (velocity/queue-depth) or the per-key/global API rate limit. |
Successful responses wrap the payload as {"data": <OutboundEmail>} (plus pagination on list endpoints). Field definitions live in /docs/openapi.yaml.
POST /api/v1/inboxes/{inbox_id}/messages/{message_id}/forward
Forward an inbound message from its inbox's verified, DKIM-verified custom domain (Business plan only).
Parameters
| Param | Type | Default | Constraint |
inbox_id |
string (path) |
required |
inbox public id |
message_id |
string (path) |
required |
message public id |
Request body
application/json
{"to": ["[email protected]"], "body_text": "...", "body_html": "..."}
Responses
| Status | Meaning |
201 |
Forward queued (status=queued); body includes a quoted, sanitized/truncated copy of the original — no attachments. |
401 |
Missing or invalid API key. |
403 |
Account is not permitted, is not Business-tier / is demo-or-agent, or the inbox's domain is not eligible to send from (sending_domain_not_eligible — same as W5). |
404 |
Unknown/foreign inbox or message (uniform, no oracle), or ENABLE_OUTBOUND is off. |
422 |
Malformed field, too many recipients, or one or more recipients are suppressed/not on the allowlist. |
429 |
outbound_rate_limited (velocity/queue-depth) or the per-key/global API rate limit. |
Successful responses wrap the payload as {"data": <OutboundEmail>} (plus pagination on list endpoints). Field definitions live in /docs/openapi.yaml.
POST /api/v1/inboxes/{inbox_id}/compose
Send a new email from an inbox's verified, DKIM-verified custom domain (Business plan only).
Parameters
| Param | Type | Default | Constraint |
inbox_id |
string (path) |
required |
inbox public id |
Request body
application/json
{"to": ["[email protected]"], "subject": "...", "body_text": "...", "body_html": "...", "cc": []}
Responses
| Status | Meaning |
201 |
Email queued (status=queued); pure passthrough over W5 with from_address pinned to the inbox's own address. |
401 |
Missing or invalid API key. |
403 |
Account is not permitted, is not Business-tier / is demo-or-agent, or the inbox's domain is not eligible to send from (sending_domain_not_eligible — same as W5). |
404 |
Unknown/foreign inbox, or ENABLE_OUTBOUND is off. |
422 |
Malformed field, too many recipients, or one or more recipients are suppressed/not on the allowlist. |
429 |
outbound_rate_limited (velocity/queue-depth) or the per-key/global API rate limit. |
Successful responses wrap the payload as {"data": <OutboundEmail>} (plus pagination on list endpoints). Field definitions live in /docs/openapi.yaml.