grabmail-io Mailbox API

Read and empty a disposable inbox.

Operations 4

GET /api/v1/mailbox List the messages waiting at an address #
GET /api/v1/message/{id} Read one message in full #
DELETE /api/v1/message/{id} Delete a message now #
GET /api/v1/attachment/{id} Download an attachment #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/grabmail-io:grabmail-io-mailbox-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

grabmail-io-mailbox-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: grabmail Mailbox API
  version: 1.0.0
  summary: Disposable email addresses with a REST API. No account, no key, no payment.
  description: 'grabmail hands out throwaway email addresses and lets you read what arrives at them over HTTP.


    There is nothing to create: an address exists the moment mail reaches it. Pick any local part on one of the public domains (grabmail.io, mixozia.com, linqmail.com), use it, then poll GET /api/v1/mailbox.


    No account, no API key and no payment are required for the public domains. Bring your own domain by pointing its MX at smtp.grabmail.io — the first message connects it, with no registration and no key.


    Messages are deleted 5 days after they arrive, and attachments are capped at 5 MB. Anyone who guesses a public address can read it, so never send anything confidential to one.


    PREMIUM POOL. The public domains are on public disposable-mail blocklists, so many signup forms reject them. A paid plan gives API access to 97 private .com domains, kept off those lists. Same endpoints, same shapes — the only difference is that a premium address requires the `Authorization: Bearer gm_live_...` key of an account on a paid plan. Without a current plan it answers 404, exactly as an unhosted domain does, so the pool cannot be enumerated by guessing names. The public domains and your own domains stay free and keyless on every plan. Premium USD 12.99/month (2,000 premium messages/month, 1 API key, 2 req/s per address, no per-minute or daily API cap); Pro USD 49.99/month (15,000 premium messages/month, 5 API keys, 3 req/s per address, no per-minute or daily API cap); Scale USD 99.99/month (36,000 premium messages/month, 20 API keys, 5 req/s per address, no per-minute or daily API cap). See https://grabmail.io/pricing


    WITH A KEY: GET /api/v1/me verifies it and shows the plan, the period, the allowance left and the rate limit (401 if the key is unknown or revoked — a bad key is refused on every endpoint, whatever the domain). GET /api/v1/domains lists the public domains for everyone and the pool''s names for a paid key only. The first authenticated read of a fresh premium address holds it for the account and counts one unit of the allowance (a unit is a message received on the pool OR an address opened, whichever comes first); reading a held address again is free, and it is released from the dashboard or when the plan lapses. Pool errors: 402 quota_reached / overage_cap / overage_funds, 403 address_taken, 409 address_limit (no plan caps addresses today). `alias` is returned for premium mailboxes too.'
  contact:
    url: https://grabmail.io/docs/api
  license:
    name: Free to use
    url: https://grabmail.io/legal/terms
servers:
- url: https://grabmail.io
  description: Production
security:
- []
- bearerAuth: []
tags:
- name: Mailbox
  description: Read and empty a disposable inbox.
paths:
  /api/v1/mailbox:
    get:
      operationId: listMessages
      tags:
      - Mailbox
      summary: List the messages waiting at an address
      description: 'Newest first. An empty mailbox is a 200 with count 0, never a 404, so a polling loop needs no special case.


        One call returns at most 200 messages; `next` carries a cursor for the page after that, and is null once there is nothing older.'
      parameters:
      - name: address
        in: query
        required: true
        schema:
          type: string
          format: email
        example: a7f3k2@grabmail.io
        description: Any local part on a hosted domain. It does not need to exist first.
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 200
          default: 50
        description: How many to return in this call. Caps the response, not the mailbox.
      - name: before
        in: query
        required: false
        schema:
          type: string
        description: 'Cursor: pass back the `next` value from the previous page.'
      responses:
        '200':
          description: The mailbox was read.
          content:
            application/json:
              schema:
                type: object
                properties:
                  address:
                    type: string
                    format: email
                  alias:
                    type: string
                    format: email
                    nullable: true
                    description: 'A second address that delivers to this mailbox and reveals nothing about it. Give it to a signup form instead of `address`: mail sent to it arrives here, and whoever holds it cannot use it to read anything — reading the alias returns an empty mailbox, always. Stable for a given address. Null when the mailbox is itself on an alias domain.'
                  count:
                    type: integer
                    description: How many are in THIS response.
                  next:
                    type:
                    - string
                    - 'null'
                    description: Cursor for the next page, or null.
                  messages:
                    type: array
                    items:
                      $ref: '#/components/schemas/MessageSummary'
                required:
                - address
                - count
                - messages
        '400':
          $ref: '#/components/responses/BadAddress'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PoolRefused'
        '403':
          $ref: '#/components/responses/PoolTaken'
        '404':
          $ref: '#/components/responses/NotHosted'
        '409':
          description: 'Premium pool: you hold as many addresses as your plan allows (`address_limit`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
  /api/v1/message/{id}:
    get:
      operationId: readMessage
      tags:
      - Mailbox
      summary: Read one message in full
      description: Body, headers and attachment list. This is where a confirmation code will be.
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      - $ref: '#/components/parameters/Mailbox'
      responses:
        '200':
          description: The message.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'
        '400':
          $ref: '#/components/responses/BadAddress'
        '404':
          description: No such message in that mailbox, or it has expired.
        '429':
          $ref: '#/components/responses/RateLimited'
    delete:
      operationId: deleteMessage
      tags:
      - Mailbox
      summary: Delete a message now
      description: 'Idempotent: deleting twice still answers 200.'
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      - $ref: '#/components/parameters/Mailbox'
      responses:
        '200':
          description: Deleted, or already gone.
        '400':
          $ref: '#/components/responses/BadAddress'
        '429':
          $ref: '#/components/responses/RateLimited'
  /api/v1/attachment/{id}:
    get:
      operationId: downloadAttachment
      tags:
      - Mailbox
      summary: Download an attachment
      description: 'Always served as application/octet-stream with Content-Disposition: attachment, whatever the sender declared. Never rendered inline.'
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
      - $ref: '#/components/parameters/Mailbox'
      responses:
        '200':
          description: The file.
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '404':
          description: No such attachment in that mailbox.
        '429':
          $ref: '#/components/responses/RateLimited'
components:
  responses:
    PoolRefused:
      description: 'Premium pool, and something about the plan says no: `quota_reached` (this period''s message quota is spent and overage is off), `overage_cap` (spent, overage on, but the ceiling you set is reached) or `overage_funds` (spent, overage on, balance too low). Addresses already held keep working in all three cases.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: 'Too many requests. Without a key: 60 requests a minute and 1000 a day per IP address (rate_limited, or daily_limit_reached for the day), and one read per second per address. A paid key has no per-minute or daily cap: 2, 3 or 5 reads per second per address. Retry-After says how long to wait; a free-limit body carries an upgrade block.'
      headers:
        Retry-After:
          schema:
            type: integer
          description: Seconds.
        X-RateLimit-Limit:
          schema:
            type: integer
          description: Free requests a minute for this IP address (calls without a key).
        X-RateLimit-Remaining:
          schema:
            type: integer
          description: Free requests left this minute.
        X-RateLimit-Daily-Limit:
          schema:
            type: integer
          description: Free requests a day for this IP address (UTC day).
        X-RateLimit-Daily-Remaining:
          schema:
            type: integer
          description: Free requests left today.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    BadAddress:
      description: The address is missing or malformed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: 'An API key was sent and it is unknown or revoked (`unauthorized`). Answered on every endpoint whatever the domain — it says something about the key, never about a domain. The public domains need no key: send none.'
      headers:
        WWW-Authenticate:
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotHosted:
      description: 'That domain is not hosted here, and its MX does not point at this service either. Check the MX record, or use a public domain. A premium-pool domain read without a current paid plan answers exactly this, deliberately: telling the two apart would let the pool be enumerated by guessing names.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    PoolTaken:
      description: 'Premium pool: another subscriber holds that address (`address_taken`).'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
          example: rate_limited
          description: Stable slug. Branch on this, never on `message`.
          enum:
          - invalid_address
          - bad_cursor
          - unknown_domain
          - not_found
          - rate_limited
          - daily_limit_reached
          - unauthorized
          - address_taken
          - address_limit
          - quota_reached
          - overage_cap
          - overage_funds
          - server_error
        message:
          type: string
          description: Prose for a human. May be reworded at any time.
        upgrade:
          type: object
          description: 'On a free-limit 429: what a paid plan removes (no per-minute or daily cap), its price, and how to subscribe.'
        hint:
          type: string
          description: 'Sometimes present: what to do about it, for a human or an agent.'
        docs:
          type: string
          format: uri
          description: 'Sometimes present: where this is explained.'
        premium_pool:
          type: object
          description: 'Sometimes present on 404 unknown_domain and 429: the premium pool, its price and how to unlock it, with `tell_the_user` — guidance for an assistant to pass the offer on to the person it works for rather than act on it.'
      required:
      - error
      - message
    Message:
      type: object
      properties:
        id:
          type: string
          example: 7ZK3QW1M8T5VDR0YXC2NHB4JPE
          description: Opaque message id. Use it with /message and /attachment.
        from:
          type: string
          format: email
          description: Envelope sender.
        from_name:
          type:
          - string
          - 'null'
          description: Display name, when the sender set one.
        subject:
          type:
          - string
          - 'null'
        date:
          type: string
          format: date-time
          description: When it arrived, UTC.
        seen:
          type: boolean
          description: True once the message has been read through the API.
        attachments:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              filename:
                type: string
              mime:
                type: string
                description: What the sender claimed. Do not trust it.
              size:
                type: integer
              url:
                type: string
                description: Always served as a download, never inline.
        expires_at:
          type: string
          format: date-time
          description: 'When it will be deleted: 5 days after it arrived.'
        to:
          type: string
          format: email
        text:
          type:
          - string
          - 'null'
          description: Plain text of the message. When the sender included a text/plain part this is that part, unchanged. When the sender shipped HTML only - which most transactional mail does - this is a plain-text rendering of it, and text_derived says so. Link targets are kept in angle brackets and image alt text is kept, so a code or a confirmation link is still here.
        text_derived:
          type: boolean
          description: false when text is the sender's own text/plain part, true when it was rendered from the HTML.
        html:
          type:
          - string
          - 'null'
          description: HTML part. Never render it unsandboxed.
    MessageSummary:
      type: object
      properties:
        id:
          type: string
          example: 7ZK3QW1M8T5VDR0YXC2NHB4JPE
          description: Opaque message id. Use it with /message and /attachment.
        from:
          type: string
          format: email
          description: Envelope sender.
        from_name:
          type:
          - string
          - 'null'
          description: Display name, when the sender set one.
        subject:
          type:
          - string
          - 'null'
        date:
          type: string
          format: date-time
          description: When it arrived, UTC.
        seen:
          type: boolean
          description: True once the message has been read through the API.
        attachments:
          type: integer
          description: How many attachments it carries.
        expires_at:
          type: string
          format: date-time
          description: 'When it will be deleted: 5 days after it arrived.'
      required:
      - id
      - from
      - date
      - seen
      - attachments
      - expires_at
  parameters:
    Mailbox:
      name: mailbox
      in: query
      required: true
      schema:
        type: string
        format: email
      example: a7f3k2@grabmail.io
      description: The address the message was delivered to. Scopes the id, so an id alone is not enough.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: 'An API key (gm_live_…), created at /account/keys — one on a free account, more on paid plans. A key has no tier of its own: each call is judged on its account''s current plan, and the same key moves with it. On the free plan it counts the free allowance per account instead of per IP address; on a paid plan it removes the per-minute and daily caps and opens the premium pool and the pool names in /api/v1/domains. The public domains and your own domains need no credential at all. A key that is unknown or revoked answers 401 wherever it is sent.'
externalDocs:
  url: https://grabmail.io/docs/api
  description: Reference with examples
x-mcp:
  endpoint: https://grabmail.io/mcp
  transport: streamable-http
  description: 'The same service as MCP tools, for AI agents: create_inbox, wait_for_message, list_messages, read_message, delete_message, list_domains. A paid key sent as Authorization: Bearer on /mcp opens the premium pool there too.'
x-public-domains:
- grabmail.io
- mixozia.com
- linqmail.com
x-retention-days: 5
x-requires-auth: false