# WarmInboxes API > Read a WarmInboxes account and place new inbox orders from your own software — over HTTPS, as an MCP server for AI assistants, or with a one-file CLI. Everything the customer portal shows is available as JSON. Base URL: https://api.warminboxes.com Version: everything is under /v1. MCP server: https://api.warminboxes.com/mcp CLI: https://api.warminboxes.com/v1/cli Human-readable docs: https://docs.warminboxes.com ## Authentication Create a key in the portal at https://app.warminboxes.com/settings. It is shown once. Send it as a bearer token on every request: ``` curl https://api.warminboxes.com/v1/portal/dashboard \ -H "Authorization: Bearer wib_xxxxxxxxxxxx_yyyyyyyy..." ``` Scopes are fixed when a key is created, and each stands alone: - `read` — every portal page as data: orders, domains, invoices, deliverability. Every GET needs it, and so does POST /v1/orders/preview. - `write` — create and rename folders, choose a sequencer, flip monitors, raise change requests (including forwarding, which repoints your domains). Every other non-GET call needs it. - `orders:place` — place new orders and charge the card on file, with nobody checking first. Also needed for the card setup link. A key cannot create or revoke keys. To rotate: create the new key, switch over, revoke the old one. Revoking takes effect on the next request and cannot be undone. ## Endpoints ### Reading - GET /v1/portal/dashboard — every domain you own, grouped by the order that paid for it, with build state and mailbox count. - GET /v1/portal/orders — your orders. - GET /v1/portal/billing — invoices, this month's spend, anything past due, and the next charge. - GET /v1/portal/analytics?period=7d — deliverability for your domains. A domain not watched yet carries null, not 0. - GET /v1/portal/workspaces — your folders and how many domains are in each. - GET /v1/portal/requests — changes you have asked for, and our replies. - GET /v1/portal/referrals — your code, what it has earned, and where it pays out. ### Changing - POST /v1/portal/workspaces — make a folder. Body: `{ "name": "..." }` - PUT /v1/portal/workspaces/{workspace_id} — rename one. - DELETE /v1/portal/workspaces/{workspace_id} — remove one. Its domains are kept, with no folder. - PUT /v1/portal/domains/{domain}/workspace — file a domain. Body: `{ "workspace_id": 12 }`, or null to unfile it. - PUT /v1/portal/orders/{subscription_id}/sequencer — set the sequencer an order sends from. - PUT /v1/portal/orders/{subscription_id}/monitors — the reconnection and deliverability switches. Send one; the other is left as it is. - POST /v1/portal/requests — ask for a change. Body: `{ "subscription_id", "kind", "summary" }`. A `forwarding` request repoints every domain on the order straight away; other kinds reach a person. ### Ordering - GET /v1/catalogue — what you can order and at what price: the plans you are already on. Other plans are not listed. - POST /v1/orders/preview — what an order would build and cost. Creates nothing. Runs the same checks as placing, so a preview that passes will be accepted. - POST /v1/orders — place it and charge the card on file. Needs `orders:place` and an `Idempotency-Key` header. - GET /v1/payment-method — whether there is a card to charge, as brand and last four. - POST /v1/payment-method/setup-link — a Stripe page where a person saves a card. Body: `{ "success_url", "cancel_url" }`. Needs `orders:place`. Send what to build, never what to charge. An order names a `sku` from your catalogue, and the domains and people to put on them. Quantity is counted from that at your plan's price. A `price`, `price_id`, `amount`, `unit_amount` or `total` in the body is refused, not ignored. ``` POST /v1/orders/preview { "sku": "price_1Q...", "promotion_code": "WELCOME10", "domains": [ { "domain": "acme-mail.com", "forward_to": "acme.com", "mailboxes": [ { "first_name": "Jane", "last_name": "Doe", "local_part": "jane" }, { "first_name": "John", "last_name": "Doe", "local_part": "john" } ] } ] } ``` A preview answers `ok`, `sku`, `product`, `domains`, `mailboxes`, `quantity`, `price` (`unit_amount`, `currency`, `interval`, `mailboxes_per_unit`, `total`, `first_invoice`), `discount` and `problems`. A refusal is a 400 whose `detail` is a sentence and whose `problems` lists every issue by domain. Placing answers `order_id`, `reference`, `subscription_id`, `quantity`, `charged` and `replayed`. Limits per order: 500 domains, 2,000 mailboxes. The API takes Google Unwarmed builds on domains you own; other plans in your catalogue say why they cannot be ordered here in `why_not`. ### Idempotency POST /v1/orders requires `Idempotency-Key`: 8 to 64 characters of letters, digits, dot, dash, colon or underscore. Retrying with the same key returns the order already placed (`"replayed": true`) instead of a second one. Use a new key for a new order. ### Promotion codes Add `promotion_code` to a preview or an order. `price.total` is what every renewal costs; `price.first_invoice` is the first invoice with the code. Many codes cover the first invoice only — `discount.applies_to` says how long. A code that cannot apply is refused, not ignored. ### The card Placing charges the card already saved on your account. If there isn't one, open the setup link once in a browser and save it there. Card details never go through the API, and an agent must never ask for them. A card is needed even when a code makes the first invoice free, because renewals are charged to it. ## Errors Every error body has a sentence in `detail`. - 400 — refused; `detail` says what to change. - 401 — key missing, not recognised, or revoked. - 402 — Stripe did not take the payment (declined card, or a code it would not redeem). Nothing was charged. - 403 — the key lacks the scope this call needs. - 404 — no such record, or no such sku on your account. - 409 — cannot go ahead as sent: no card on file, the price changed since your preview, or the plan cannot be ordered this way yet. Nothing was charged. - 422 — a return URL or field is not in the expected shape. - 429 — too many requests. Wait `Retry-After` seconds. - 502 — Stripe could not be reached. Nothing was charged; retry. - 503 — sign-in checks unavailable. Retry. ## Rate limits Per key; ordering is per account. - 240 / minute — reading: every GET, and every MCP message. - 60 / minute — changing: anything that is not a GET. - 12 / hour — placing orders, shared by the API, MCP and CLI on one account. Every JSON answer carries `X-RateLimit-Remaining`. A refusal is 429 with `Retry-After` in seconds — wait that long. Orders count whether or not the card is accepted, so a loop retrying a declined card runs the allowance down. Need more? Ask, and we will raise it. ## MCP server Streamable HTTP, stateless, at https://api.warminboxes.com/mcp with your key as a bearer token. Claude Code: ``` claude mcp add --transport http warminboxes https://api.warminboxes.com/mcp \ --header "Authorization: Bearer wib_your_key_here" ``` Clients that take a JSON config: ``` { "mcpServers": { "warminboxes": { "url": "https://api.warminboxes.com/mcp", "headers": { "Authorization": "Bearer wib_your_key_here" } } } } ``` Tools (an assistant sees only those its key's scopes allow): - get_dashboard — your domains, grouped by order. - list_orders — your orders. - get_billing — invoices, spend, and what is due next. - get_deliverability — how your domains are sending. - list_catalogue — what you can order, and at what price. - check_payment_method — whether there is a card to charge. - preview_order — check and price an order. Creates nothing. - create_card_setup_link — a Stripe page for a person to save a card. Needs orders:place. - place_order — places it and charges the card. Needs orders:place. To let an assistant quote without ordering, give it a key without `orders:place`. ## CLI `wib` is one Python 3 file with no install, for scripts and agents that have a shell. ``` curl -fsSL https://api.warminboxes.com/v1/cli -o wib && chmod +x wib export WIB_API_KEY=wib_your_key_here ./wib catalogue ./wib preview order.json ./wib order order.json --idempotency-key monday-1 --yes ``` Commands: `catalogue`, `preview`, `order` (needs `--yes`), `payment-method`, `setup-link`, `dashboard`, `orders`, `billing`, `deliverability --period 7d`, `requests`, `workspaces`, and `get` / `post` for any other path. Every command prints JSON and exits 0. A refusal prints the reason on stderr and exits 1, so `wib … || handle` works. `--api` and `--key` override `WIB_API_URL` and `WIB_API_KEY`.