API reference

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.

curl -s https://grabmail.io/api/v1/domains \
  -H "Authorization: Bearer gm_live_…"

{
  "public": ["grabmail.io", "mixozia.com", "linqmail.com"],
  "premium": {
    "count": 97,
    "unlocked": true,
    "domains": ["one-of-the-pool.com", "another-of-the-pool.com", "…"]
  }
}

Checking a key

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 401 unauthorized 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.

curl -s https://grabmail.io/api/v1/me \
  -H "Authorization: Bearer gm_live_…"

{
  "plan": "premium",
  "status": "active",
  "premium": true,
  "period": { "start": "2026-09-14T00:00:00Z", "end": "2026-10-14T00:00:00Z" },
  "usage": { "messages": 12, "addresses": 5, "consumed": 12, "quota": 2000, "left": 1988, "over": false },
  "addresses_held": 5,
  "limits": { "requests_per_second": 2, "requests_per_minute": null, "requests_per_day": null, "api_keys": 1, "address_cap": null },
  "pool": { "count": 97, "access": true, "domains": "GET https://grabmail.io/api/v1/domains" }
}

What the pool answers

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

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

Example

List a mailbox
$ curl -G https://grabmail.io/api/v1/mailbox \
  --data-urlencode "address=k7fq2m@grabmail.io"

{
  "address": "k7fq2m@grabmail.io",
  "alias": "hq8kd2m4p7zt@alias-domain.example",
  "count": 1,
  "next": null,
  "messages": [
    {
      "id": "01JR8W2K4Q",
      "from": "no-reply@example.com",
      "subject": "Your verification code",
      "date": "2026-08-04T18:31:07Z",
      "seen": false,
      "attachments": 0,
      "expires_at": "2026-08-09T18:31:07Z"
    }
  ]
}

Status codes

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

Parameters

NameInTypeRequiredDescription
id path string yes The message id returned by the list call.
mailbox query string yes The address the message was delivered to.

Example

Read a message
$ curl -G https://grabmail.io/api/v1/message/01JR8W2K4Q \
  --data-urlencode "mailbox=k7fq2m@grabmail.io"

{
  "id": "01JR8W2K4Q",
  "from": "no-reply@example.com",
  "to": "k7fq2m@grabmail.io",
  "subject": "Your verification code",
  "date": "2026-08-04T18:31:07Z",
  "text": "Your code is 481920. It expires in 10 minutes.",
  "html": null,
  "attachments": []
}

Status codes

200
The message. html is null when the sender sent plain text only.
400
mailbox is missing or invalid.
404
No such message in that mailbox — or it has passed its retention window.
429
Rate limit exceeded.
DELETE /api/v1/message/{id}

Removes it at once, rather than waiting for the retention window to expire.

Parameters

NameInTypeRequiredDescription
id path string yes The message to remove.
mailbox query string yes The address it was delivered to.

Example

Delete a message
$ curl -X DELETE -G https://grabmail.io/api/v1/message/01JR8W2K4Q \
  --data-urlencode "mailbox=k7fq2m@grabmail.io"

{
  "deleted": true,
  "id": "01JR8W2K4Q"
}

Status codes

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

Connect a domain →

Welcome back

Your inboxes and your domains, in one place.

Menu

Product

Developers

Company

Legal

Language

Free · No signup · Deleted after 5 days

New address

Public inbox — anyone with the address can read it.