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.
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.
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.
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.
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 freeget_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.
| ceiling | limit | means |
|---|---|---|
| per_recipient_day | 5 | messages one recipient may receive per day, from anyone |
| per_key_hour | 30 | messages per API key per hour |
| per_owner_day | 200 | messages per account per day |
| global_day | 500 | messages across this deployment per day |
Every error code
| http | error | when |
|---|---|---|
| 400 | bulk_not_supported | to was an array. There is no bulk verb — send one message per call. |
| 400 | to_required / invalid_recipient | Missing or unparseable recipient address. |
| 400 | reply_to_required / invalid_reply_to | Every message needs a reply path, because Mailroom does not receive. |
| 400 | subject_required / subject_too_long | Subject missing, or longer than 200 characters. |
| 400 | header_injection | A CR, LF or NUL in subject or from_name. Those would let a caller append their own headers. |
| 400 | body_required / text_too_long / html_too_long / body_too_long | Missing body, or over the size budget. |
| 401 | unauthorized | No 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. |
| 402 | insufficient_credits | Carries required_credits, balance_credits and buy_credits_url. The reserve is refused, so you can never overdraw. |
| 402 | payment_invalid (x402 only) | The signature did not verify, or the settlement was refused. Nothing was sent, or nothing was charged. |
| 403 | recipient_suppressed | The recipient is on Ounie's shared do-not-contact list. Free, and no price was quoted. |
| 429 | rate_limited | Carries ceiling, limit and retry_after_seconds. Free, and no price was quoted. |
| 502 | send_failed | The provider refused the message. Credits are refunded in full; on x402 nothing was settled. |
| 503 | send_rail_closed | Sending is not open on this deployment. Free, and no price is quoted. |
| 503 | upstream_unavailable | A scheduled probe found sending unavailable. Free, and no price is quoted. |
Consent, complaints and keys
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.comAvailability
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.