Robotomail mascotThis page is for AI agents

Set up your email

Follow these steps to create a Robotomail account and get a real email address you can use to send and receive email autonomously.

Base URL: https://api.robotomail.com

01

Create an account

One POST creates your account, your default mailbox, and returns an API key. Save the api_key.key value immediately — it is only shown once. If lost, the account owner can sign in and create a replacement (see Key Recovery below). The slug becomes your permanent platform email (slug@robotomail.co) and cannot be changed. A verification email is sent to the signup address — ask your human to click the link before you try to send email.

POST /v1/signup
POST /v1/signup
Content-Type: application/json

{
  "email": "you@company.com",
  "password": "a-secure-password",
  "slug": "my-company"
}

# Response (201):
# {
#   "user": { "slug": "my-company", "platform_email": "my-company@robotomail.co" },
#   "api_key": { "key": "rm_a1b2c3d4..." },
#   "mailbox": {
#     "id": "mbx_abc123",
#     "address": "my-company",
#     "fullAddress": "my-company@robotomail.co",
#     "status": "ACTIVE"
#   },
#   "mailbox_limit": 3,
#   "daily_send_limit": 100,
#   "monthly_send_limit": 5000,
#   "email_verified": false,
#   "next_steps": {
#     "verify_email": "Ask your human to click the link sent to the signup email — required before sending",
#     "add_domain": "POST /v1/domains",
#     "send_email": "POST /v1/mailboxes/:id/messages"
#   }
# }
#
# Header: X-RateLimit-Remaining: <number>
02

Your mailbox is ready

Your default mailbox (slug@robotomail.co) was created automatically at signup. All subsequent requests require the Authorization header. You can create webhooks and upload attachments immediately — no email verification needed. Additional platform mailboxes are not available on the free plan; upgrade or add a custom domain for more mailboxes. Sending email requires the account owner to verify their email first (a link was sent to the signup address). If you get a 403 when sending, ask your human to check their inbox and click the verification link. Set a display name so your emails show "Your Name" instead of just the email address.

GET /v1/mailboxes
GET /v1/mailboxes
Authorization: Bearer rm_a1b2c3d4...

# Response (200):
# {
#   "mailboxes": [
#     {
#       "id": "mbx_abc123",
#       "address": "my-company",
#       "fullAddress": "my-company@robotomail.co",
#       "status": "ACTIVE",
#       "dailySendLimit": 100,
#       "monthlySendLimit": 5000,
#       "receivedThisMonth": 42
#     }
#   ]
# }
# receivedThisMonth is the per-mailbox inbound count for the current
# calendar month (UTC). The account-level cap is the sum across mailboxes.

# Set a display name (emails will show "Hal" <my-company@robotomail.co>):
PATCH /v1/mailboxes/:id
Authorization: Bearer rm_a1b2c3d4...
Content-Type: application/json

{
  "displayName": "Hal"
}
03

Send email

Send to any email address. Threading is automatic — include inReplyTo (camelCase) to continue a conversation. Free tier: 100 sends/day and 5,000 sends/month per mailbox. When either limit is hit, the API returns 429.

POST /v1/mailboxes/:id/messages
POST /v1/mailboxes/:id/messages
Authorization: Bearer rm_a1b2c3d4...
Content-Type: application/json

{
  "to": ["recipient@example.com"],
  "subject": "Hello from my agent",
  "bodyText": "This is the email body."
}
04

Receive email

Three options: (A) Register a webhook for real-time push notifications — best if your agent has a public URL. Webhook payloads are { event, timestamp, data } objects with snake_case fields. (B) SSE for real-time streaming — no public URL needed, works behind firewalls. Use the CLI or connect directly. (C) Poll the messages endpoint on a schedule — simplest option for batch processing. Polling returns stored message objects with camelCase fields. Note: the formats differ — see the docs for the exact shapes. Inbound messages with files (PDFs, images, etc.) include an attachments[] array on the payload — webhook/SSE payloads ship a ready-to-use download_url for each file (presigned R2 URL, valid 24h). For inline images, match content_id against cid: references in body_html.

Receive email
# Option A: Webhooks (real-time push)
POST /v1/webhooks
Authorization: Bearer rm_a1b2c3d4...
Content-Type: application/json

{
  "url": "https://your-server.com/webhook",
  "events": ["message.received"],
  "headers": {
    "Authorization": "Bearer your-token"
  }
}

# Response (201):
# {
#   "webhook": {
#     "id": "wh_abc123",
#     "url": "https://your-server.com/webhook",
#     "events": ["message.received"],
#     "secret": "a1b2c3d4e5f6..."
#   }
# }
# Save the secret — it is only returned on creation.

# Option B: SSE (real-time stream — no public URL needed)
# Use the CLI: robotomail listen --forward-to http://localhost:4000/hook
# Or connect directly:
GET /v1/events?events=message.received
Authorization: Bearer rm_a1b2c3d4...
Accept: text/event-stream
# Events stream as Server-Sent Events. Reconnect with Last-Event-ID
# to replay missed events. See docs for all 5 event types.

# Option C: Polling (simplest for batch processing)
GET /v1/mailboxes/:id/messages?direction=INBOUND&since=2026-03-11T00:00:00Z&limit=10
Authorization: Bearer rm_a1b2c3d4...

# Poll every 10-30 seconds. Use direction=INBOUND and since
# with an ISO timestamp to get new inbound messages.
# Track the latest createdAt to avoid reprocessing.

# ─── Inbound attachments ────────────────────────────────────────
# Webhook/SSE message.received payload shape:
# {
#   "event": "message.received",
#   "timestamp": "2026-03-18T12:00:00.000Z",
#   "data": {
#     "message_id": "...",
#     "from": "...",
#     "body_html": "<img src=\"cid:logo@acme\" />...",
#     "attachments": [
#       {
#         "id": "att_...",
#         "filename": "invoice.pdf",
#         "content_type": "application/pdf",
#         "size_bytes": 204800,
#         "content_id": null,
#         "download_url": "https://...r2.cloudflarestorage.com/...?signed-24h"
#       }
#     ]
#   }
# }
#
# Fetch each attachment with HTTP GET on download_url — no auth header needed.
# For inline images (content_id != null), rewrite cid:<content_id> in body_html
# to download_url before passing the HTML to a renderer.
#
# REST list/get does NOT include download_url — call GET /v1/attachments/:id
# to fetch a fresh presigned URL (also use this if a download_url has expired).
#
# Drop limits: max 20 attachments per message, 25 MB each. If exceeded,
# attachments_dropped: true and attachments_dropped_reason: "size"|"count"|"both".
#
# ─── Quota-gated payloads (over monthly inbound limit) ───────────
# Monthly inbound caps: Free 100 / Developer 2,000 / Growth 20,000 /
# Scale unlimited. When your account is over its cap, every subsequent
# inbound message still fires a message.received event — but the data
# object drops to a stub (identity fields only, no body/attachments):
# {
#   "event": "message.received",
#   "timestamp": "2026-03-18T12:00:00.000Z",
#   "data": {
#     "over_limit": true,
#     "message_id": "...",
#     "mailbox_id": "...",
#     "mailbox_address": "...",
#     "received_at": "...",
#     "account": {
#       "inbound_usage": { "current": 101, "limit": 100, "percentage": 101, "reset_date": "2026-05-01T00:00:00.000Z", "status": "limit_reached" },
#       "upgrade": { "browser_url": "https://robotomail.com/billing", "api_endpoint": {"method":"POST","path":"/v1/billing/upgrade"}, "hint": "..." }
#     }
#   }
# }
# Branch on data.over_limit === true. Stubs carry NO from/to/subject/body/attachments.
#
# While over-limit: GET /v1/mailboxes/:id/messages/:msgId and
# GET /v1/attachments/:id return 402 INBOUND_LIMIT_EXCEEDED until
# you upgrade or the monthly reset lands. List endpoints
# (GET /v1/mailboxes/:id/messages, /threads) still return 200 —
# they hide over-limit messages and expose the count at
# metadata.overLimitCount. Upgrade with POST /v1/billing/upgrade;
# previously gated messages become readable via the list endpoint.
# They are NOT retroactively redelivered via webhook/SSE.
#
# List envelope metadata (GET messages / GET threads):
# {
#   "metadata": {
#     "overLimitCount": 3,
#     "upgrade": {                    # always present (camelCase)
#       "browserUrl": "https://robotomail.com/billing",
#       "apiEndpoint": {"method":"POST","path":"/v1/billing/upgrade"},
#       "hint": "..."
#     },
#     "limitHint": {                  # present once usage ≥ 50 % (snake_case,
#       "current": 80,                # matches webhook/SSE inbound_usage)
#       "limit": 100,
#       "percentage": 80,
#       "reset_date": "2026-05-01T00:00:00.000Z",
#       "status": "near"              # approaching | near | limit_reached
#     }
#   }
# }
#
# Approaching the limit (≥ 50 %): full payloads carry a
# data.account.inbound_usage block (no upgrade sub-field yet).
# Use it to surface a countdown.
05

Verify webhook signatures

Every webhook payload includes an X-Robotomail-Signature header. Compute HMAC-SHA256 of the raw request body using your webhook secret and compare.

Webhook verification
# Verification pseudocode:
expected = HMAC-SHA256(webhook_secret, raw_request_body)
signature = request.headers["X-Robotomail-Signature"]
valid = timing_safe_equal(expected, signature)

# Reject the payload if valid is false.
06

Error handling

The API returns structured errors. Always check the status code and handle these common cases programmatically.

Common error responses
# 402 — Monthly inbound limit reached (INBOUND_LIMIT_EXCEEDED)
# Returned by GET /v1/mailboxes/:id/messages/:msgId and GET /v1/attachments/:id
# when the requested inbound resource was received after the cap was reached.
# {
#   "error": "Monthly inbound limit reached — upgrade to unlock this message",
#   "code": "INBOUND_LIMIT_EXCEEDED",
#   "resource": { "type": "message", "id": "..." },
#   "upgrade": { "browserUrl": "...", "apiEndpoint": { "method": "POST", "path": "/v1/billing/upgrade" } },
#   "resetAt": "2026-05-01T00:00:00.000Z"
# }
# Headers: Retry-After (seconds to reset), X-Robotomail-Retriable: false,
#   X-Robotomail-Inbound-Quota: 101/100, X-Robotomail-Inbound-Warning: reached.
# → Do NOT retry on backoff. Upgrade or wait for the monthly reset.
# → List endpoints still return 200 with over-limit messages hidden and counted in metadata.overLimitCount.
# → Webhook/SSE payloads for over-limit messages arrive as stubs with data.over_limit: true (no body, no attachments).

# 429 — Rate limited (daily or monthly send limit exceeded)
# {
#   "error": "Daily send limit reached",
#   "upgrade": "POST /v1/billing/upgrade to get a checkout URL"
# }
# → Wait until midnight UTC for daily reset, the 1st of the month for monthly reset, or upgrade to Developer for 500/day and 15,000/month.

# 403 — Forbidden (email not verified)
# {
#   "error": "Email verification required before sending. Check your inbox."
# }
# → Ask the account owner to verify their email.

# 403 — Mailbox limit reached
# {
#   "error": "Free plan limited to 3 mailboxes. Upgrade to create more.",
#   "upgrade": "POST /v1/billing/upgrade to get a checkout URL"
# }
# → Upgrade to paid, or delete an existing mailbox.

# 409 — Conflict (duplicate resource)
# {
#   "error": "A mailbox with this address already exists"
# }
# → Use a different address, or list existing mailboxes.
07

Check account status

Call GET /v1/account to see your current plan, limits, verification state, and what actions are needed. Check this endpoint when you hit unexpected errors to understand your account state.

GET /v1/account
GET /v1/account
Authorization: Bearer rm_a1b2c3d4...

# Response:
# {
#   "account": {
#     "id": "usr_abc123",
#     "email": "you@company.com",
#     "emailVerified": true,
#     "slug": "my-company",
#     "plan": "free",
#     "suspended": false,
#     "suspendedReason": null,
#     "storageUsedBytes": 0,
#     "storageLimitBytes": 1073741824,
#     "mailboxCount": 1,
#     "activeMailboxCount": 1,
#     "sentToday": 12,
#     "sentThisMonth": 240,
#     "receivedToday": 5,
#     "bouncedToday": 0,
#     "monthlyInboundCount": 42,
#     "monthlyInboundLimit": 100,
#     "monthlyInboundPercentage": 42,
#     "monthlyInboundResetAt": "2026-05-01T00:00:00.000Z",
#     "overLimitMessageCount": 0
#   }
# }
#
# Requires a full-access API key (scoped keys get 403).
# Use this to check verification, plan, daily/monthly send usage,
# monthly inbound quota, and suspension status.
08

Key recovery

If you lose your API key, you can create a new one by signing in with your email and password. The new key is returned once — persist it immediately. Revoke the lost key afterwards.

Key recovery
# 1. Sign in with email/password to get a session
POST /api/auth/sign-in/email
Content-Type: application/json

{
  "email": "you@company.com",
  "password": "a-secure-password"
}

# 2. Create a new API key (uses session cookie from sign-in)
POST /v1/api-keys
Cookie: <session cookie from step 1>
Content-Type: application/json

{
  "name": "replacement-key"
}

# Response: { "key": "rm_newkey...", "prefix": "rm_newk" }

# 3. Revoke the lost key
DELETE /v1/api-keys/:id
Authorization: Bearer rm_newkey...
09

Upgrade your plan

When you hit free-tier limits (429 on send, 403 on mailbox creation, or 402 INBOUND_LIMIT_EXCEEDED after 100 inbound messages this month), request a checkout URL and share it with your human to complete payment. Pass a plan name to select a tier. The URL expires in 24 hours. Once paid, limits lift automatically and any gated inbound messages unlock in place.

POST /v1/billing/upgrade
POST /v1/billing/upgrade
Authorization: Bearer rm_a1b2c3d4...
Content-Type: application/json

{"plan": "developer"}
# Plans: "developer" ($19/mo), "growth" ($79/mo), "scale" ($199/mo)
# Omit body to default to "developer".
#
# New session (201):
# {
#   "checkout_url": "https://checkout.stripe.com/c/pay/...",
#   "expires_at": 1741737600,
#   "message": "Share this URL with the account owner..."
# }
#
# Existing session (200) — safe to call repeatedly.
# Already paid? Returns 409.
10

Delete account

Permanently delete the account and all associated data (mailboxes, messages, domains, attachments, webhooks, API keys). Requires full-access authentication (unscoped API key or dashboard session — scoped keys are rejected). A confirmation body is required. External service cleanup (storage, email provider, billing) is best-effort after the database deletion.

DELETE /v1/account
DELETE /v1/account
Authorization: Bearer rm_a1b2c3d4...
Content-Type: application/json

{
  "confirm": "DELETE"
}

# Response (200):
# { "deleted": true }
#
# This is irreversible. All data is permanently removed.
# 400 — Missing or invalid confirmation body
# 403 — Scoped key (requires full-access)

Free tier limits

  • 3 mailboxes
  • 100 sends per day, 5,000 per month (429 when exceeded)
  • 100 inbound messages per month across all mailboxes (over-limit inbound returns 402 INBOUND_LIMIT_EXCEEDED on single-read endpoints; list endpoints hide them and count in metadata.overLimitCount; webhook/SSE payloads arrive as stubs with over_limit: true)
  • Platform email only (slug@robotomail.co — slug is permanent)
  • 1 GB attachment storage