Developer API · v1 · private beta

Temporary email API for automated testing.

Create an isolated receiving address, trigger the workflow under test, and retrieve the expected message. Every API inbox is scoped to the credential that created it.

Authenticate with a bearer token

The full token is displayed only when the operator creates it; 9Mail.xyz stores its hash. Load the token from your CI secret store or local environment—never from browser code.

Authorization: Bearer $NINEMAIL_API_TOKEN
Accept: application/json

Create an isolated inbox

Both domain_id and local_part are optional. Omit them to use an available domain and a random local part. Omit ttl_minutes to use the current default of about 60 minutes.

curl --fail-with-body \
  --request POST \
  --url "https://9mail.xyz/api/v1/inboxes" \
  --header "Authorization: Bearer $NINEMAIL_API_TOKEN" \
  --header "Accept: application/json" \
  --header "Content-Type: application/json" \
  --data '{"ttl_minutes": 60}'

201 response

{
  "data": {
    "uuid": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "address": "[email protected]",
    "domain": "mail.example.test",
    "email_count": 0,
    "expires_at": "2026-07-15T14:00:00+00:00",
    "expires_in_seconds": 3600
  }
}

Values above are illustrative. Use the returned UUID and address; do not construct either value yourself.

List messages until the expected one arrives

Use the UUID from the create response. Results are newest first and cursor-paginated; pass meta.next_cursor as cursor when it is not null.

curl --fail-with-body \
  --get "https://9mail.xyz/api/v1/inboxes/$INBOX_UUID/messages" \
  --data-urlencode "per_page=25" \
  --header "Authorization: Bearer $NINEMAIL_API_TOKEN" \
  --header "Accept: application/json"

The current v1 surface is polling-based and does not expose a webhook endpoint. Do not poll in a tight loop: use a deadline, increase the delay between empty responses, and honor the Retry-After header after a 429 response.

Reference

Four focused endpoints.

Resources created by one credential are not readable by another. Cross-credential lookups return a not-found response.

POST /api/v1/inboxes

Create a temporary inbox owned by the current credential.

Requires inbox:create

GET /api/v1/inboxes/{inbox_uuid}/messages?per_page=25&cursor=…

List message summaries with cursor pagination.

Requires inbox:read

GET /api/v1/messages/{message_uuid}

Retrieve message bodies, metadata, and attachment summaries.

Requires inbox:read

GET /api/v1/messages/{message_uuid}/attachments/{attachment_uuid}

Download an attachment owned by the credential.

Requires inbox:read

verification-email.spec.js

Bounded polling with Playwright

Example
import { test, expect } from '@playwright/test';

const apiBase = 'https://9mail.xyz/api/v1';
const token = process.env.NINEMAIL_API_TOKEN;
const pause = ms => new Promise(resolve => setTimeout(resolve, ms));

if (!token) throw new Error('NINEMAIL_API_TOKEN is required');

test('signup sends a verification email', async ({ page, request }) => {
  const headers = {
    Authorization: `Bearer ${token}`,
    Accept: 'application/json',
  };

  const create = await request.post(`${apiBase}/inboxes`, {
    headers,
    data: { ttl_minutes: 60 },
  });
  expect(create.status()).toBe(201);
  const { data: inbox } = await create.json();

  // Replace this URL and these selectors with the product under test.
  await page.goto('https://app.example.test/signup');
  await page.getByLabel('Email').fill(inbox.address);
  await page.getByRole('button', { name: 'Create account' }).click();

  const deadline = Date.now() + 30_000;
  let delayMs = 1_000;
  let summary;

  while (Date.now() < deadline) {
    const list = await request.get(
      `${apiBase}/inboxes/${inbox.uuid}/messages?per_page=25`,
      { headers },
    );

    if (list.status() === 410) throw new Error('Test inbox expired');

    if (list.status() === 429) {
      const retryMs = Number(list.headers()['retry-after'] ?? 1) * 1_000;
      await pause(Math.min(retryMs, 5_000, Math.max(0, deadline - Date.now())));
      continue;
    }

    expect(list.ok()).toBeTruthy();
    const payload = await list.json();
    summary = payload.data.find(message => message.subject === 'Verify your email');
    if (summary) break;

    await pause(Math.min(delayMs, Math.max(0, deadline - Date.now())));
    delayMs = Math.min(Math.ceil(delayMs * 1.5), 5_000);
  }

  expect(summary, 'verification email did not arrive before the deadline').toBeTruthy();

  const detail = await request.get(`${apiBase}/messages/${summary.uuid}`, { headers });
  expect(detail.ok()).toBeTruthy();
  const { data: message } = await detail.json();

  expect(message.to_addresses.some(recipient => recipient.email === inbox.address)).toBeTruthy();
  expect(message.text_body).toContain('Verify');
});
01

Expiry and cleanup

Listing an expired inbox returns 410 Gone. Cleanup runs asynchronously, so never depend on an expired inbox, message, or attachment remaining available.

02

Errors and rate limits

Expect 401 for missing or invalid credentials, 403 for a missing ability, 404 for unavailable or differently-owned resources, 410 for an expired inbox listing, 422 for validation, and 429 for rate limits.

03

Request tracing

Every API response includes X-Request-ID. Authenticated responses also expose X-RateLimit-Limit and X-RateLimit-Remaining; 429 responses include Retry-After.

Treat credentials and message content as untrusted boundaries

Keep API tokens out of browser bundles, public repositories, screenshots, logs, and support messages. Give each credential only the abilities it needs. Do not execute email HTML or scripts, validate extracted URLs before following them, and scan attachments according to your own security policy. Never send production secrets or real sensitive data through a test inbox.

Legacy routes

Build new integrations on v1.

Legacy /api/inbox routes are deprecated and send deprecation metadata. They are scheduled to sunset on 14 October 2026. New code should use the documented /api/v1 endpoints.

Optional analytics help us improve 9Mail.xyz. They stay off until you accept. Privacy details