Skip to content

The REST API

Everything the dashboard does, your agent can do over REST. Every request goes to mail.sairaph.com/api/v1. Send an email with one POST, read inbound with one GET, read a single message by its id.

Full reference ↗

Authentication and keys

Issue a key from the dashboard and send it as a Bearer token on every request. An sm_live_ key is scoped to one mailbox and carries a read or read_write role, for REST and MCP calls against that mailbox. An sm_mgmt_ key is tenant-wide and carries the management role, for account-level operations. A management key is minted read-only by default; request write scopes explicitly (for example mailboxes:write or domains:write) when it needs to create or change resources. Set an expiry of 30 days, 90 days, or 1 year; mailbox keys can also be set to never expire, management keys always expire. List or revoke keys at any time; a revoke takes effect almost immediately.

Idempotent writes

Send is a POST to /api/v1/mailboxes/{id}/outbound and returns 202. It requires an Idempotency-Key header, so a retried request never sends the same email twice. Reuse the same key on a retry to get the original result back.

cURL

Structured errors

Every error returns the same envelope: type, code, message, and an optional param. The shape follows Stripe, so you can branch on it in code.

404 mailbox_not_found

Error codes

The codes you branch on most, with the HTTP status each returns and how to recover. Match on error.code; it is stable. message is for humans.

invalid_api_keyHTTP 401
The Authorization Bearer token is missing, malformed, or unknown. Check you are sending a current sm_live_… / sm_mgmt_… key.
idempotency_key_requiredHTTP 400
Send a unique Idempotency-Key header on the outbound POST - it is mandatory so a retry can never double-send.
message_body_requiredHTTP 400
Include body_text or body_html (at least one) in the send body.
attachment_count_exceededHTTP 429
Too many attachments on one message. Split the send across fewer attachments per message.
mailbox_not_foundHTTP 404
No mailbox matches that id for this owner (siblings are deliberately not enumerable). Check the mbx_… id.
mailbox_not_activeHTTP 409
The mailbox is still provisioning or suspended. Wait for it to reach active, or check its status in the dashboard.
mailbox_send_disabledHTTP 409
Sending is disabled on this mailbox by the bounce-rate guard rail. Contact support to re-enable.
mailbox_domain_not_foundHTTP 404
The mailbox's domain is not provisioned/verified yet. Finish domain setup before sending.
payment_requiredHTTP 402
The subscription is unpaid or cancelled, so the send path is gated (inbound reads are never gated). Update billing to resume sending.

Rate limits

Each API key is rate-limited per minute, scaled by your plan. This limit is separate from your monthly outbound cap; both apply on their own. Exceed the per-minute limit and you get a 429 with a Retry-After header. Per-tier limits below read live from your plan.

Starter

60 requests/min per key

Agent

120 requests/min per key

Entrepreneur

240 requests/min per key

Swarm

600 requests/min per key

Reading inbound

List inbound with GET /api/v1/mailboxes/{id}/messages?direction=inbound. Bodies are served from cache by default. Add ?live=true for an on-demand fetch from the source. Live fetches are rate-limited per mailbox to protect the upstream IMAP service, and fall back to cached data if the source is briefly unreachable. A single fetched message (envelope plus body) is capped at 25 MiB.

With ?live=true a page can mix object types. Each row carries an object discriminator: either cached_message (it came from the cache) or live_message (a fresh fetch). Branch on it to know how current a row is.

cURL

List rows carry a snippet. To read one message in full, call GET /api/v1/mailboxes/{id}/messages/{msg_id} with the id from a list row. It returns the quote-stripped body by default; add include=quoted for the full thread and include=html for the raw HTML.

cURL

Pagination

List endpoints return the Stripe-style envelope: object: "list", data, has_more, and next_cursor. When has_more is true, pass the returned next_cursor back as ?cursor= on the inbound reads endpoint to fetch the next page. Outbound lists page with ?starting_after=.

Full reference

The complete OpenAPI reference is generated from our live schema and browsable as interactive Swagger at /docs.

Start building