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