WarmInboxes developers

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.

Ask what an order would cost
$ curl https://api.warminboxes.com/v1/orders/preview \
    -H "Authorization: Bearer $WIB_API_KEY" \
    -H "Content-Type: application/json" \
    -d @order.json
Nothing is created
{
  "ok": true,
  "domains": 1,
  "mailboxes": 2,
  "quantity": 2,
  "price": {
    "total": 7.0,
    "first_invoice": 7.0,
    "interval": "month"
  },
  "problems": []
}

Reading this as an agent? /llms.txt has this whole reference as plain Markdown.

On this page
Base URLhttps://api.warminboxes.com
VersionEverything is under /v1.
FormatJSON 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.

ScopeWhat it allows
readEvery portal page as data: orders, domains, invoices and deliverability. Every GET needs it, and so does previewing an order.
writeCreate and rename folders, choose a sequencer, flip monitors, and raise change requests, including forwarding, which repoints your domains.
orders:placePlace 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, not 0.
  • 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 }, or null to 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" }. A forwarding request 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:place and an Idempotency-Key header.
  • 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" }. Needs orders: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.json

Promotion 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.

StatusMeaning
400The request was refused. detail says what to change.
401The key is missing, not recognised, or revoked.
402Stripe did not take the payment: a declined card, or a code it would not redeem. Nothing was charged.
403The key does not have the scope this call needs.
404No such record, or no such sku on your account.
409The 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.
422A return URL or field is not in the shape expected.
429Too many requests. Retry-After says how many seconds to wait.
502Stripe could not be reached. Nothing was charged; retry.
503Sign-in checks are unavailable. Retry.

Rate limits

Counted per key, except ordering, which is counted per account.

AllowanceCovers
240 / minuteReading: every GET, and every message an AI assistant sends.
60 / minuteChanging: anything that is not a GET.
12 / hourPlacing 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" }
    }
  }
}
ToolWhat it does
get_dashboardYour domains, grouped by order.
list_ordersYour orders.
get_billingInvoices, spend, and what is due next.
get_deliverabilityHow your domains are sending.
list_catalogueWhat you can order, and at what price.
check_payment_methodWhether there is a card to charge.
preview_orderCheck and price an order. Creates nothing.
create_card_setup_linkA Stripe page for a person to save a card. Needs orders:place.
place_orderPlace 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
CommandWhat it does
catalogueWhat you can order, and at what price.
previewCheck and price an order. Creates nothing.
orderPlace it and charge the card. Needs --yes.
payment-methodWhether there is a card to charge.
setup-linkA Stripe page to save a card.
dashboard, orders, billingThe reading screens, as JSON.
deliverabilityHow your domains are sending. --period 7d.
requests, workspacesYour change requests and folders.
get, postAny 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.