Mailroom

API reference

Send one message. Read your log. That is the API.

Three rails — REST, MCP and keyless x402 — over one set of rules. Every rail refuses the same inputs for the same reasons, so an integration that works on one behaves identically on the others.

The shape

One recipient per call. to is a string. Passing an array returns 400 bulk_not_supported rather than silently taking the first address. There is no bulk verb, no recipient list and no campaign endpoint, on any rail.

reply_to is required. Mailroom sends but does not receive, so the From address has no inbox behind it. A message a person cannot reply to is a dead end, and requiring a reply path is also a real cost on abuse.

not offered

Recipient lists, bulk sends, campaigns, sequences

`to` is a single address and there is no bulk verb. This is deliberate and permanent: an agent-callable bulk sender is a spam cannon. Ounie's campaign product is Sequences, which requires an account and a human.

not offered

Inboxes, receiving, reading replies

Mailroom sends. It does not receive. There is no address to provision, no message to poll, and no webhook for inbound mail — set `reply_to` to an address you already read.

not offered

Attachments

Not offered in v1 rather than half-offered. A parameter that works for some inputs and silently fails for others is worse than one that is absent.

not offered

Open tracking, click tracking, link rewriting

Nothing is injected into your message and no link is rewritten. What you send is what arrives.

REST

POST /api/sends

Sends one message. Costs 2 credits. Authenticate with an mbx_live_… key, the fleet master key, or the shared Ounie session cookie.

curl -X POST https://mailbox.ounie.com/api/sends \
  -H "Authorization: Bearer mbx_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "to":        "someone@example.com",
    "reply_to":  "you@yourdomain.com",
    "subject":   "Your report is ready",
    "text":      "The nightly run finished. Summary attached below.",
    "html":      "<p>The nightly run finished.</p>",
    "from_name": "Nightly Reports"
  }'

# 200
{ "ok": true, "send": {
    "id": "…", "to": "someone@example.com", "status": "sent",
    "message_id": "0100019…", "credits_spent": 2,
    "body_bytes": 94, "sent_at": "…" } }

Limits: subject 200 characters, text 100,000, html 200,000. Supply text, html, or both.

GET /api/sends · GET /api/sends/<id>

Your own send log, free. The message body is not returned because it was never stored — only the recipient, subject, size, timestamps and the key that sent it.

curl "https://mailbox.ounie.com/api/sends?limit=20&status=sent" \
  -H "Authorization: Bearer mbx_live_…"

MCP

Streamable HTTP at /api/mcp, legacy SSE at /api/sse. Works with Claude, Cursor, ChatGPT and the Ounie AI Team. That last one cannot set static headers on an HTTP MCP entry, so the key rides the URL as ?api_key=.

https://mailbox.ounie.com/api/mcp
Authorization: Bearer mbx_live_…

# or, for hosts that cannot set headers:
https://mailbox.ounie.com/api/mcp?api_key=mbx_live_…

Tools
  send_email            2 credits · one message, one recipient
  get_send              free · one entry from your log
  list_sends            free · your recent sends
  get_sending_policy    free, no auth · the limits and every refusal, up front
  get_credit_balance    free · your shared Ounie wallet
  get_pricing           free, no auth
  whoami                free

get_sending_policy is public and unauthenticated on purpose. An agent that discovers a limit only by tripping it will keep tripping it, so the limits are readable before the first call.

x402 — keyless, pay per call

Agents with a wallet and no Ounie account pay in USDC on Base: $0.024 per message. POST without a payment header and you get a 402 carrying the terms; sign the EIP-3009 authorization and retry with X-Payment.

curl -X POST https://mailbox.ounie.com/api/x402/send \
  -H "Content-Type: application/json" \
  -d '{"to":"someone@example.com","reply_to":"you@x.com",
       "subject":"hello","text":"hi"}'

# 402
{ "x402Version": 1, "accepts": [{
    "scheme": "exact", "network": "base",
    "maxAmountRequired": "24000",
    "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "payTo": "0x…",
    "extra": { "name": "USD Coin", "version": "2" } }] }

curl -X POST https://mailbox.ounie.com/api/x402/send \
  -H "X-Payment: <base64 signed payload>" \
  -H "Content-Type: application/json" -d '{ … }'

Ordering matters. The route verifies your signature off-chain, sends the message, and settles only once the provider has accepted it. An x402 settlement is final on chain and there is no refund, so a failure has to be caught before the money moves rather than corrected after.

A refusal never carries a quote. A suppressed recipient, a tripped ceiling, a malformed message or an unavailable upstream all return a 4xx or 5xx with no accepts and no payTo. Making you build and sign an authorization for a send that was always going to be refused would waste a wallet round-trip on a guaranteed failure.

This rail is send-only. A keyless caller has no account to hold an address against and no way to be held responsible for what arrives, so there is no inbox here — and, in v1, none anywhere.

Limits and refusals

Every ceiling below is checked before a price exists. Tripping one is a 429 with ceiling, limit and retry_after_seconds, and no charge on either rail.

ceilinglimitmeans
per_recipient_day5messages one recipient may receive per day, from anyone
per_key_hour30messages per API key per hour
per_owner_day200messages per account per day
global_day500messages across this deployment per day

Every error code

httperrorwhen
400bulk_not_supportedto was an array. There is no bulk verb — send one message per call.
400to_required / invalid_recipientMissing or unparseable recipient address.
400reply_to_required / invalid_reply_toEvery message needs a reply path, because Mailroom does not receive.
400subject_required / subject_too_longSubject missing, or longer than 200 characters.
400header_injectionA CR, LF or NUL in subject or from_name. Those would let a caller append their own headers.
400body_required / text_too_long / html_too_long / body_too_longMissing body, or over the size budget.
401unauthorizedNo session cookie and no valid Bearer key. Never a 302 — this route is outside the auth proxy matcher on purpose, so an agent gets a status it can act on.
402insufficient_creditsCarries required_credits, balance_credits and buy_credits_url. The reserve is refused, so you can never overdraw.
402payment_invalid (x402 only)The signature did not verify, or the settlement was refused. Nothing was sent, or nothing was charged.
403recipient_suppressedThe recipient is on Ounie's shared do-not-contact list. Free, and no price was quoted.
429rate_limitedCarries ceiling, limit and retry_after_seconds. Free, and no price was quoted.
502send_failedThe provider refused the message. Credits are refunded in full; on x402 nothing was settled.
503send_rail_closedSending is not open on this deployment. Free, and no price is quoted.
503upstream_unavailableA scheduled probe found sending unavailable. Free, and no price is quoted.

Every message carries List-Unsubscribe and List-Unsubscribe-Post (RFC 8058). The link is one-click for clients that support it and a one-button confirmation page for everyone else — a bare GET never suppresses, because link scanners fetch every URL in every message before a human sees it.

Unsubscribes, hard bounces and spam complaints all land in Ounie's shared do-not-contact list, which covers every Ounie mail rail rather than just this one. A key whose recipients file 2 complaints within 30 days is revoked automatically, with the reason shown on the api-keys page. Sending reputation is shared, so the cost of one abusive integration would otherwise land on everybody else.

Testing without mailing a person

Send to the provider's mailbox simulator. It is a real send, billed like one.

  success@simulator.amazonses.com
  bounce@simulator.amazonses.com
  complaint@simulator.amazonses.com
  suppressionlist@simulator.amazonses.com

Availability

A scheduled probe sends a real message to the mailbox simulator to confirm sending still works — never a status read, because a provider can answer every read with a 200 while refusing every write. While the probe says sending is unavailable, both rails refuse up front with a 503 and charge nothing, and no quote is issued. Recovery is detected by the same probe, with no deploy.

Credits

2 credits per message from your shared Ounie wallet. Buy them at ounie.com/dashboard/settings. Reserve-before-send means a call can be refused for insufficient credits but can never overdraw, and a message the provider refuses is refunded pool-exact.

Mint a keyPricing