API reference

REST API reference (v1)

Every endpoint below is served under /api/v1 and authenticated only by an Authorization: Bearer <key> header — dashboard session cookies are ignored. API keys are prefixed ms_live_. The raw machine-readable contract is available at /docs/openapi.yaml.

Authentication

Send your API key as a Bearer token on every request:

Authorization header
curl -H "Authorization: Bearer ms_live_..." "https://dash.mailsocket.app/api/v1/inboxes"

Missing or invalid keys return 401 with WWW-Authenticate: Bearer. Keys are hashed at rest and individually revocable from the dashboard.

Rate limits

Every response carries rate-limit headers describing the matched per-key bucket:

HeaderMeaning
X-RateLimit-LimitThe ceiling for the matched bucket.
X-RateLimit-RemainingRequests remaining in the current window.
X-RateLimit-ResetUnix timestamp (seconds) when the window resets.
Retry-AfterSeconds to wait before retrying (present on 429).

Exceeding the limit returns 429 with code rate_limited — see the error reference.

Cursor pagination

List endpoints (inboxes, inboxes/{id}/messages) page with an opaque cursor instead of page numbers. Each page's response has a pagination object:

Pagination object
{
  "pagination": {
    "next_cursor": "opaque-signed-cursor",
    "has_more": true
  }
}
  • Pass the returned next_cursor back as the cursor query parameter to fetch the next page.
  • When has_more is false, next_cursor is null and there are no more rows.
  • The cursor is signed and bound to its owner and the exact filter set — a cursor minted under one filter cannot be replayed under another.

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

StatusMeaning
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

application/json
{"email": "[email protected]"}

Responses

StatusMeaning
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

StatusMeaning
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

ParamTypeDefaultConstraint
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

StatusMeaning
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

ParamTypeDefaultConstraint
limit integer 25 1–100
cursor string — opaque, signed, owner- and filter-bound

Responses

StatusMeaning
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

StatusMeaning
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

ParamTypeDefaultConstraint
id string (path) required inbox public id, e.g. inbox_...

Responses

StatusMeaning
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

ParamTypeDefaultConstraint
id string (path) required inbox public id, e.g. inbox_...

Responses

StatusMeaning
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

ParamTypeDefaultConstraint
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

StatusMeaning
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

ParamTypeDefaultConstraint
id string (path) required inbox public id

Responses

StatusMeaning
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

ParamTypeDefaultConstraint
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

StatusMeaning
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

ParamTypeDefaultConstraint
id string (path) required message public id, e.g. msg_...

Responses

StatusMeaning
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

ParamTypeDefaultConstraint
email string (path) required a shared-domain address, e.g. [email protected]
limit integer 20 1–20

Responses

StatusMeaning
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

ParamTypeDefaultConstraint
id string (path) required domain public id

Responses

StatusMeaning
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

StatusMeaning
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

ParamTypeDefaultConstraint
id string (path) required outbound email public id

Responses

StatusMeaning
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

StatusMeaning
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

application/json
{"email": "[email protected]"}

Responses

StatusMeaning
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

ParamTypeDefaultConstraint
id string (path) required approved-recipient public id

Responses

StatusMeaning
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

StatusMeaning
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

ParamTypeDefaultConstraint
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

StatusMeaning
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

ParamTypeDefaultConstraint
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

StatusMeaning
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

ParamTypeDefaultConstraint
inbox_id string (path) required inbox public id

Request body

application/json
{"to": ["[email protected]"], "subject": "...", "body_text": "...", "body_html": "...", "cc": []}

Responses

StatusMeaning
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.