Receiving/Test inbox API

Markdown

Test inbox API

Updated Oct 22, 2026 · 3 min read

Catch test emails from any email provider and wait for them in your tests. Your team has a test domain, <team>.test.groundmail.dev. Mail sent to any address there is kept in your test inbox for 14 days and never delivered to anyone. Emails you send with a gm_test_ key land here too.

For the walkthrough with Playwright, Vitest, Jest, Cypress and pytest, see Email testing.

Base URL: https://api.groundmail.dev

Auth (any key with the emails:read scope, test keys included):

Endpoint What it does
GET /test-inbox Your test domain, an example address and this month's usage
GET /test-inbox/emails Caught emails, newest first, filtered by to and since
GET /test-inbox/emails/{id} One email with bodies, headers and source
GET /test-inbox/wait Long-poll until an email to to arrives (up to 60 seconds)

Every email carries code and links, extracted for you:

  • code: the one-time code in the email, 4 to 8 digits (123456, also written 123 456 or 123-456), preferring one next to words like "code" or "verification". Years, prices, times and order numbers don't count. null when there is none.
  • links: every http(s) link in the email, in order, without duplicates (the HTML's links, or the URLs in the text when there is no HTML).
HTTP
Authorization: Bearer gm_...

Get your test domain

GET/test-inbox

The first call creates your team's test domain. address is a fresh example address on every call.

usage.included is 100 a month on every account, or 5,000 with the email testing add-on. Without the add-on, test emails past the limit are refused (usage.rejected) until resets_at. With it, they are accepted and billed per 1,000 (usage.overage: true). Emails sent with a gm_test_ key are free and don't count.

Terminal
curl https://api.groundmail.dev/test-inbox \
  -H "Authorization: Bearer $GROUNDMAIL_API_KEY"
JSON
{
  "test_inbox": {
    "object": "test_inbox",
    "domain": "acme.test.groundmail.dev",
    "address": "run-k3m9x2pq@acme.test.groundmail.dev",
    "addon": { "active": false, "status": null },
    "usage": {
      "month": "2026-10",
      "received": 42,
      "rejected": 0,
      "included": 100,
      "overage": false,
      "resets_at": "2026-11-01T00:00:00.000Z"
    },
    "retention_days": 14
  }
}

List test emails

GET/test-inbox/emails
Query Description
to Only emails to this exact address
since Only emails received at or after this time (ISO 8601)
limit 1–100, default 25
cursor next_cursor from the previous page

source is inbound for mail received at your test domain, and test_key for an email you sent through Groundmail with a gm_test_ key (email_id is then that email's id).

Terminal
curl "https://api.groundmail.dev/test-inbox/emails?to=signup-k3m9x2pq@acme.test.groundmail.dev" \
  -H "Authorization: Bearer $GROUNDMAIL_API_KEY"
JSON
{
  "emails": [
    {
      "object": "test_inbox_email",
      "id": "tie_3f9c2a1b7d8e4f6a0b1c2d3e",
      "source": "inbound",
      "email_id": null,
      "from": "Acme <no-reply@acme.com>",
      "to": ["signup-k3m9x2pq@acme.test.groundmail.dev"],
      "cc": [],
      "subject": "Your Acme verification code",
      "code": "482913",
      "links": ["https://app.acme.com/verify?token=abc"],
      "received_at": "2026-10-22T10:00:00.000Z",
      "expires_at": "2026-11-05T10:00:00.000Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Retrieve a test email

GET/test-inbox/emails/{id}

Returns { "email": … }: the list fields plus text_body, html_body, headers ([{ "name", "value" }] in message order) and raw_source (the raw message, when available).

404 not_found when there is no such email for your team (or it expired).

Terminal
curl https://api.groundmail.dev/test-inbox/emails/tie_3f9c2a1b7d8e4f6a0b1c2d3e \
  -H "Authorization: Bearer $GROUNDMAIL_API_KEY"

Wait for a test email

GET/test-inbox/wait

Holds the request open until an email to to arrives, then returns it like GET /test-inbox/emails/{id}. An email that already arrived is returned at once, so it doesn't matter whether your test calls this before or after your app sends.

Query Description
to Required. The exact address your app sent to
since Only emails received at or after this time (ISO 8601). Useful when one address receives several emails
timeout Seconds to wait, default 30, at most 60

When several emails match, the newest is returned. If nothing arrives in time, the response is 408 with code timeout:

Calling again is safe. The SDK's testInbox.waitFor repeats the call for timeouts longer than 60 seconds.

Terminal
curl "https://api.groundmail.dev/test-inbox/wait?to=signup-k3m9x2pq@acme.test.groundmail.dev&timeout=30" \
  -H "Authorization: Bearer $GROUNDMAIL_API_KEY"
JSON
{
  "error": {
    "message": "No email to signup-k3m9x2pq@acme.test.groundmail.dev arrived within 30 seconds",
    "code": "timeout",
    "hint": "Nothing arrived in time. Check that your app sent the email to the exact address you are waiting on, then call again: the wait is safe to repeat and returns an email that arrived in the meantime. Waits last at most 60 seconds per call.",
    "docs_url": "https://docs.groundmail.dev/errors#timeout",
    "details": { "to": "signup-k3m9x2pq@acme.test.groundmail.dev", "timeout": 30 }
  }
}

SDK, CLI and MCP

MCP tools: get_test_inbox_address, wait_for_test_email, list_test_emails, get_test_email.

TypeScript
import { Groundmail } from 'groundmail'

const groundmail = new Groundmail(process.env.GROUNDMAIL_API_KEY!)

const to = await groundmail.testInbox.address({ prefix: 'signup' }) // signup-k3m9x2pq@acme.test.groundmail.dev
const email = await groundmail.testInbox.waitFor({ to, timeout: 30_000 })
console.log(email.code, email.links)

await groundmail.testInbox.list({ to })
await groundmail.testInbox.get(email.id)
await groundmail.testInbox.info() // domain and usage
Terminal
TO=$(groundmail test-inbox address --prefix signup)
groundmail test-inbox wait --to "$TO" --timeout 30
groundmail test-inbox list --to "$TO"

Was this page helpful?