Order inboxes from code.
Everything the WarmInboxes portal shows you is available as JSON, and new Google inbox orders can be placed without opening it. Call the REST API, give an AI assistant the MCP server, or run the one-file wib command line.
$ curl https://api.warminboxes.com/v1/orders/preview \
-H "Authorization: Bearer $WIB_API_KEY" \
-H "Content-Type: application/json" \
-d @order.json
{
"ok": true,
"domains": 1,
"mailboxes": 2,
"quantity": 2,
"price": {
"total": 7.0,
"first_invoice": 7.0,
"interval": "month"
},
"problems": []
}
$ ./wib preview order.json
{ "ok": true, "quantity": 2,
"price": { "total": 7.0, ... } }
You: Price 2 more inboxes on
acme-mail.com, Jane and John.
Claude: That adds 2 units at $3.50,
$7.00 a month. Nothing has
been ordered yet.
Reading this as an agent? /llms.txt has this whole reference as plain Markdown.
On this page
| Base URL | https://api.warminboxes.com |
| Version | Everything is under /v1. |
| Format | JSON in, JSON out. Errors carry a sentence in detail. |
Keys and scopes
Create a key in the portal under Settings. It is shown once, so copy it then. 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 the key is created, and each stands alone: a key gets exactly the ones you tick.
| Scope | What it allows |
|---|---|
| read | Every portal page as data: orders, domains, invoices and deliverability. Every GET needs it, and so does previewing an order. |
| write | Create and rename folders, choose a sequencer, flip monitors, and raise change requests, including forwarding, which repoints your domains. |
| orders:place | Place new orders and charge the card on file with nobody checking first. Only give it to a system you would trust to spend money. |
A key cannot create or revoke keys. To rotate one, create the new key, switch your system over, then revoke the old one. Revoking takes effect on the next request and cannot be undone.
Reading
The portal's screens, as data. All need read.
- GET/v1/portal/dashboardEvery domain you own, grouped by the order that paid for it, with its build state and mailbox count.
- GET/v1/portal/ordersYour orders.
- GET/v1/portal/billingInvoices, this month's spend, anything past due, and the next charge.
- GET/v1/portal/analytics?period=7dDeliverability for your domains. A domain not being watched yet carries
null, not0. - GET/v1/portal/workspacesYour folders, and how many domains are in each.
- GET/v1/portal/requestsChanges you have asked for, and our replies.
- GET/v1/portal/referralsYour code, what it has earned, and where it pays out.
Changing
All need write.
- POST/v1/portal/workspacesMake a folder.
{ "name": "Q4 outbound" } - 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}/workspaceFile a domain into a folder.
{ "workspace_id": 12 }, ornullto unfile it. - PUT/v1/portal/orders/{subscription_id}/sequencerSet the sequencer an order sends from.
- PUT/v1/portal/orders/{subscription_id}/monitorsThe reconnection and deliverability switches. Send one and the other is left as it is.
- POST/v1/portal/requestsAsk for a change:
{ "subscription_id", "kind", "summary" }. Aforwardingrequest repoints every domain on the order straight away; the other kinds reach a person.
Ordering
Preview first, then place. A preview runs exactly the checks placing runs, so one that passes will be accepted.
- GET/v1/catalogueWhat you can order and at what price: the plans you are already on. Other plans are not listed.
- POST/v1/orders/previewWhat an order would build and cost. Creates nothing. Needs
read. - POST/v1/ordersPlace it and charge the card on file. Needs
orders:placeand anIdempotency-Keyheader. - GET/v1/payment-methodWhether there is a card to charge, as brand and last four digits.
- POST/v1/payment-method/setup-linkA Stripe page where a person saves a card.
{ "success_url", "cancel_url" }. Needsorders:place.
Send what to build, not what to charge
An order names a sku from your catalogue and the domains and people to put on them. The 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.
{
"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 quantity, price (unit_amount, total, first_invoice, interval), 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, subscription_id, charged and replayed.
One order takes up to 500 domains and 2,000 mailboxes. The API builds Google Unwarmed inboxes on domains you own; any other plan in your catalogue says why it cannot be ordered here in why_not.
Retry with the same Idempotency-Key
POST /v1/orders requires an Idempotency-Key header: 8 to 64 letters, digits, dots, dashes, colons or underscores. Sending the same key again returns the order you already placed, with "replayed": true, instead of placing a second one. Use a new key for a new order.
curl https://api.warminboxes.com/v1/orders \
-H "Authorization: Bearer $WIB_API_KEY" \
-H "Idempotency-Key: acme-2026-09-24-1" \
-H "Content-Type: application/json" \
-d @order.jsonPromotion codes
Add promotion_code to a preview or an order. price.total is what every renewal costs, and price.first_invoice is the first invoice with the code. Many codes cover the first invoice only; discount.applies_to says how long one lasts. 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. 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 that says what happened and what to change.
| Status | Meaning |
|---|---|
| 400 | The request was refused. detail says what to change. |
| 401 | The key is missing, not recognised, or revoked. |
| 402 | Stripe did not take the payment: a declined card, or a code it would not redeem. Nothing was charged. |
| 403 | The key does not have the scope this call needs. |
| 404 | No such record, or no such sku on your account. |
| 409 | The order 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 shape expected. |
| 429 | Too many requests. Retry-After says how many seconds to wait. |
| 502 | Stripe could not be reached. Nothing was charged; retry. |
| 503 | Sign-in checks are unavailable. Retry. |
Rate limits
Counted per key, except ordering, which is counted per account.
| Allowance | Covers |
|---|---|
| 240 / minute | Reading: every GET, and every message an AI assistant sends. |
| 60 / minute | Changing: anything that is not a GET. |
| 12 / hour | Placing orders. The API, an assistant and the command line share one allowance per account. |
Every JSON answer carries X-RateLimit-Remaining, so you can slow down before you are refused. A refusal is a 429 with Retry-After in seconds; wait that long rather than retrying at once. Orders count whether or not the card is accepted, so a loop retrying a declined card runs the allowance down.
Building something that needs more? Tell us what it does and we will raise your limits. They exist to stop runaway loops, not to ration you.
MCP server
An assistant that supports the Model Context Protocol can read your account and, if the key allows, place orders. The server is stateless Streamable HTTP at one URL, 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 configured with JSON, such as Cursor:
{
"mcpServers": {
"warminboxes": {
"url": "https://api.warminboxes.com/mcp",
"headers": { "Authorization": "Bearer wib_your_key_here" }
}
}
}| Tool | What it does |
|---|---|
| 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 | Place it and charge the card. Needs orders:place. |
An assistant sees only the tools its key's scopes allow. It can order only plans you are already on, at your price, but it can still get the quantity wrong. To let it quote without ordering, give it a key without orders:place.
Command line
wib is one Python 3 file with nothing to install, for scripts and agents that have a shell rather than an MCP connector.
$ 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| Command | What it does |
|---|---|
| catalogue | What you can order, and at what price. |
| preview | Check and price an order. Creates nothing. |
| order | Place it and charge the card. Needs --yes. |
| payment-method | Whether there is a card to charge. |
| setup-link | A Stripe page to save a card. |
| dashboard, orders, billing | The reading screens, as JSON. |
| deliverability | How your domains are sending. --period 7d. |
| requests, workspaces | Your change requests and folders. |
| get, post | Any other path, for what has no command. |
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.