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.

One case still carries a header: a domain we have closed on request is read with Authorization: Bearer <key>, and a wrong or missing key answers 401 with unauthorized. Keys are compared in constant time, so a wrong one takes as long to reject as a right one takes to accept.

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 92 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 valid key the same request answers 402 or 403 — never a mailbox.

# A public domain: no header at all.
curl -sG https://grabmail.io/api/v1/mailbox \
  --data-urlencode "address=a7f3k2@grabmail.io"

# A premium domain: the same call, plus a key.
curl -sG https://grabmail.io/api/v1/mailbox \
  -H "Authorization: Bearer gm_live_…" \
  --data-urlencode "address=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. Quotas count messages that arrive on 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",
  "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.
400
The address is malformed, or before is not a message id.
400
address is missing or is not a valid address.
404
That domain is not hosted here — check the MX record.
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, unknown_domain, not_found and rate_limited.

Rate limits

One request per second, per address. Polling a mailbox once a second is the intended pattern and is never throttled.

Over the limit you get 429 with Retry-After in seconds. There is no daily quota and no burst credit to manage.

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.