Three endpoints, JSON in and JSON out. No key and no account on public
domains — paste a request into a terminal and it works.
Overview
A mailbox is never created — it exists the moment a message arrives at an
address, and is gone 5 days later. There is
nothing to register, so on public domains the API has no concept of a
user, a project or a token.
Every response is JSON, including every error.
All times are UTC in RFC 3339 form — 2026-08-04T18:31:07Z.
Message ids are opaque strings. Do not parse them.
Receive only. There is no endpoint that sends mail, by design.
Base URL
https://grabmail.io/api/v1
HTTPS only; plain HTTP is redirected. The version lives in the path, and
v1 will not change shape under you — a breaking change gets
a new number.
Authentication: none
None on public domains. Anyone who knows an address can
read its mailbox, through the API exactly as through the website. That is
the bargain a shared disposable service makes, so never point anything
you care about at a public address.
A domain you point here answers on these same endpoints, with no key
either. Point the MX at us and the first message connects it; see
connecting a domain. Mailboxes on it are readable by anyone who knows the address,
exactly like the public domains.
The only credential in this API is your account’s key, described below — one on a free account, more on paid plans. A key has no tier of its own: each call is judged on the plan its account has at that moment, so the same key loses the free caps and opens the premium pool as soon as you subscribe, and goes back to the free allowance when the plan ends. The public domains need no key; sent with them, it counts your calls against your account instead of your IP address. A key that is sent and is unknown or revoked answers 401 with unauthorized on every endpoint, whatever the domain — so a typo shows up at once rather than as a mailbox that looks empty.
Premium domains
The one exception to the rule above. The public domains
are on the public disposable-mail blocklists, which is why a signup form
sometimes refuses an address on them. A paid plan opens a pool of
97 private .com domains, kept off those lists.
Nothing about the API changes. Same paths, same
parameters, same response shapes. The only difference is one header:
a premium address is read with
Authorization: Bearer gm_live_…, using a key from
your API keys. Without a key from a paid plan the same request answers
404 — word for word the answer for a domain we do not
host, deliberately, so the pool cannot be enumerated by guessing
names. Never a mailbox.
Where the pool's domains are
GET /api/v1/domains with your key returns the public
domains and the pool's domain names. The same call without a key —
or with a key from a free or lapsed account — returns the public
domains, how many domains the pool holds and what it costs, but
never a name: keeping them off the disposable-mail
blocklists is what the plan buys. A signed-in subscriber also sees
the pool in the domain picker on the homepage.
GET /api/v1/me is the one call that judges a key on its
own: 200 with the plan, the current period, the allowance
consumed and left, the addresses held and your rate limit — or
401unauthorized if the key is unknown or
revoked. Call it first when something on the pool answers
404: that answer hides a key problem on purpose, this
one shows it.
Every case, and the reason for it. The first two are the ones that
look surprising until the reason is read.
404
unknown_domain — no key, or a key from a free or lapsed account. Identical to the answer for a domain we do not host, deliberately: telling the two apart would let the pool be listed by guessing names. Check the key with /api/v1/me.
401
unauthorized — the key sent is unknown or revoked. Answered on every endpoint, whatever the domain: it says something about the key, never about a domain.
402
quota_reached — this period's allowance is spent and overage is off. overage_cap / overage_funds — overage is on, but the ceiling you set is reached or the balance is short. Addresses already held keep working in all three cases.
403
address_taken — another subscriber holds that address. Pick another name.
409
address_limit — reserved for a plan that caps addresses. No plan does today.
429
rate_limited — on the free plan (no key, or a free one), more than 60 requests a minute, or more than one read a second on one address; with a paid key, more than 2, 3 or 5 reads a second on one address. daily_limit_reached — the 1,000 free requests of the day are used up. Paid plans have neither the per-minute nor the daily cap. Both carry Retry-After.
Held addresses and the allowance
A premium address is held by the first account that
reads it with a valid key — any read, on any endpoint or through MCP,
nothing to reserve beforehand. While held, only that account can read
it; anyone else gets 403. It stays held until you release
it from the dashboard, or the plan ends — on its end date, with no
grace period.
The allowance counts units: a message received on a
premium address or a fresh premium address opened, whichever
comes first — so an address you open and never read still uses one.
Reading a held address again is free. No plan caps how many addresses
an account holds; the allowance is what bounds it, and
/api/v1/me shows what is left.
The alias, on premium addresses too
Every mailbox listing carries an alias: a second address
that delivers to the same mailbox and reveals nothing about it. Give
the alias to a form and poll the address; whoever holds the alias sees
an empty mailbox, always. It is stable for a given address, free, and
returned for premium addresses exactly as for public ones. Its domain
is one of ours and is not listed anywhere on purpose.
The whole thing, in four calls
KEY="gm_live_…" # from your API keys page, with a paid plan
# 1. Check the key: plan, allowance left, rate limit. 401 if it is bad.
curl -s https://grabmail.io/api/v1/me -H "Authorization: Bearer $KEY"
# 2. List the pool. The names are in the answer only with a paid key.
curl -s https://grabmail.io/api/v1/domains -H "Authorization: Bearer $KEY"
# 3. Read a mailbox on one of them. This first read holds the address and counts one unit.
curl -sG https://grabmail.io/api/v1/mailbox -H "Authorization: Bearer $KEY" \
--data-urlencode "address=a7f3k2@one-of-the-pool.com"
# 4. Read one message, same header.
curl -sG https://grabmail.io/api/v1/message/MESSAGE_ID -H "Authorization: Bearer $KEY" \
--data-urlencode "mailbox=a7f3k2@one-of-the-pool.com"
The public domains and any domain of your own stay free, keyless and
unlimited on every plan, including the free one. The allowance counts
the premium pool and nothing else. Plans and prices are
on the plans page.
Endpoints
GET/api/v1/mailbox
Everything waiting at an address, newest first. This is the call your test suite polls.
Parameters
Name
In
Type
Required
Description
address
query
string
yes
The mailbox to read, e.g. k7fq2m@grabmail.io.
limit
query
integer
no
How many messages to return in this call, 1–200. Defaults to 50, newest first. It caps one response, not the mailbox — use before to read past it.
before
query
string
no
The id of the oldest message you already have; returns the page after it. Pass back the next field from the previous response. When next is null, you have everything.
The mailbox was read. An empty mailbox is a 200 with count: 0, never a 404. next carries the cursor for the following page, or null at the end. alias is a second address that delivers here and reveals nothing about this one — give it to a form instead of address; whoever holds it sees an empty mailbox, always. It is stable, free, and returned for premium addresses too.
400
The address is malformed, or before is not a message id.
400
address is missing or is not a valid address.
401
An API key was sent and it is unknown or revoked. A bad key is refused on every domain; the public domains need none.
402
Premium pool only: the plan's allowance is spent — quota_reached, or overage_cap / overage_funds when overage is on. Addresses already held keep working.
403
Premium pool only: another subscriber holds that address (address_taken).
404
That domain is not hosted here — check the MX record. A premium-pool domain read without a key from a paid plan answers exactly this, on purpose.
429
Rate limit exceeded. Retry after the delay in Retry-After.
GET/api/v1/message/{id}
Headers, the plain-text part, the HTML part and any attachments.
Deleted. The call is idempotent: deleting twice still answers 200.
400
mailbox is missing or invalid.
404
No such message in that mailbox.
429
Rate limit exceeded.
Attachments
Every message lists its attachments with a ready-made URL. Fetch it with
the same authorisation as the message itself.
GET /api/v1/attachment/{id}?mailbox={address}
It always answers application/octet-stream with
Content-Disposition: attachment, whatever the sender labelled
it. That is deliberate: echoing a stranger's text/html back
would let an attachment run as a page on this origin. The real type is in
the message JSON, where it is data rather than an instruction.
Errors
Every failure is JSON with the same two fields, so a client handles them
in one place. The status carries the category, error is a
stable machine-readable slug, and message is for humans and
may be reworded at any time.
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "invalid_address",
"message": "address must look like name@domain"
}
Never branch on message. The slugs in use are invalid_address, bad_cursor, unknown_domain, not_found, rate_limited, daily_limit_reached, unauthorized and, on the premium pool only, quota_reached, overage_cap, overage_funds, address_taken and address_limit.
Some errors carry more than the two fields: a hint saying what to do, a docs link, on 404 and 429 a premium_pool block with the pool's price and how to unlock it, and on a free-limit 429 an upgrade block saying what a paid plan removes — written for an assistant that reads the body on somebody's behalf, so it can pass the option on rather than give up.
Rate limits
Free, without a key: 60 requests a minute and 1,000 a day, per IP address. Every call on a mailbox counts — listing it, reading a message, its HTML body, an attachment, a delete — and so does every MCP tool call. The day resets at 00:00 UTC.
With a free API key, the same allowance follows your account instead of your IP address — useful when your calls come from changing addresses, like a CI runner. A free account has one key, and the mailboxes it reads are listed in your inboxes. Create a free key
Within that, one read per second per address: polling a mailbox once a second is the intended rhythm.
Paid plans have no per-minute cap and no daily cap. Send the key of a paid plan as Authorization: Bearer and only the per-address rate of the plan remains — 2, 3 or 5 requests per second, on every domain, counted per account. Premium starts at $12.99 a month. See the plans
The inbox on this website does not draw on the free API allowance: a page left open keeps checking for mail for as long as you need it.
Over a limit you get 429 with a Retry-After header in whole seconds: rate_limited for the minute or the address, daily_limit_reached for the day, with the time left until midnight UTC. Every free response says where you stand: X-RateLimit-Remaining for the minute, X-RateLimit-Daily-Remaining for the day.
Retention
A message is deleted 5 days
after it arrives, read or not. Every message carries
expires_at, so you never compute that date yourself.
It is a hard limit, not a setting — no parameter extends it. If a message
has to outlive the window, fetch it and store it on your side.
Your own domain
Point your MX at smtp.grabmail.io and every address on
your domain answers through these same endpoints — no second API to learn,
no registration, and no key.