Mail API

NULL mail is a disposable email service at https://mail.nullnet.sh. You create an inbox, receive mail at it, read the mail over HTTP or a WebSocket, and the inbox expires on its own. The web app does everything through the public HTTP API at https://mail.nullnet.sh/v1, so your own code can do the same.

You can use inboxes without an account. An anonymous session token is enough. If you want inboxes that last longer, more inboxes, or premium features, create an account. Accounts are anonymous too: an account is a random account code plus a password. There is no email address, phone number or name on file, and no way to reset a password by email.

The machine-readable contract is the OpenAPI 3 document at /v1/openapi.json. It is generated from the handlers.

Conventions

  • Base URL: https://mail.nullnet.sh/v1. Request and response bodies are JSON (Content-Type: application/json).
  • Ids (inboxes, messages, webhooks, API keys, domains, sessions) are UUID strings.
  • Timestamps are RFC 3339 strings in UTC, for example 2026-10-26T08:00:00Z.
  • Sizes are integers in bytes. Durations are integers in seconds. The field name gives the unit (size_bytes, retention_secs).
  • Money is USD as a decimal string with 18 fractional digits, for example "4.990000000000000000". The server never uses floating point. See Billing.
  • If an endpoint's body is optional, you can send no body, an empty body or the JSON literal null.
  • An inbox or message that belongs to someone else returns 404, never 403, so ids cannot be probed.

Authentication

There are three ways to call the API. A few endpoints need no credentials at all: GET /v1/domains, GET /v1/inboxes/available, GET /v1/billing/plans, GET /v1/billing/assets and GET /v1/passes/public-key.

  • Name
    Anonymous session
    Type
    Authorization: Bearer
    Description

    POST /v1/session returns a session token. Send it as Authorization: Bearer <token>. The session owns the inboxes it creates. Nobody else can reach them. If you lose the token, you lose the inboxes.

  • Name
    Account session
    Type
    Authorization: Bearer
    Description

    POST /v1/accounts (register) or POST /v1/auth/login returns the token of a logged-in session. You send it the same way. Inboxes created while logged in belong to the account and are visible from every session of that account. The account, billing, referral, webhook, custom-domain and API-key endpoints need this kind of session.

  • Name
    API key
    Type
    X-API-Key
    Description

    Premium accounts can create API keys (grk_…) and send them as X-API-Key: <key>. A key acts for its account on the inbox, message and paste-pass endpoints only. A key has a read scope (GET, HEAD) and a write scope (every other method). See API keys.

Session tokens are 43-character base64url strings. A session lasts 30 days, and the expiry moves forward while the session is in use. Never put a token in a URL. The WebSocket also takes the token in its first message, not in the URL.

If a request carries both headers, the bearer token is used. A missing, expired or revoked token returns 401 unauthorized.

Authenticated requests

curl https://mail.nullnet.sh/v1/inboxes \
  -H "Authorization: Bearer {token}"

curl https://mail.nullnet.sh/v1/inboxes \
  -H "X-API-Key: {api_key}"

Tiers and limits

Every request resolves to one tier. The limits come from server configuration. Premium tiers are more generous because their configured numbers are higher. API responses use these tier names:

  • Name
    anon_ip
    Description

    No credentials. Limits are counted per client IP.

  • Name
    anon_session
    Description

    An anonymous session token.

  • Name
    free_account
    Description

    A logged-in account without an active premium subscription.

  • Name
    premium_account
    Description

    A logged-in account with an active premium subscription.

  • Name
    premium_api_key
    Description

    An API key of a premium account. Limits are counted per key.

These are the current production limits for the tiers that can own inboxes:

Limitanon_sessionfree_accountpremium_accountpremium_api_key
API requests per minute (burst)60 (20)120 (40)1,200 (200)3,000 (500)
Live inboxes at once35200500
New inboxes per hour10305001,000
Inbox retention1 hour24 hours30 days30 days
Messages kept per inbox5020010,00010,000
Storage per inbox25 MiB100 MiB2 GiB2 GiB
Largest accepted message10 MiB10 MiB50 MiB50 MiB
Inbound messages per inbox per hour1003005,0005,000
Address availability checks per minute1020120300
Attachment downloads per hour1203005,00010,000
Raw and export downloads per hour20601,0002,000
Full-text searches per minute–10120300
WebSocket connections per session2320–
Subscriptions per WebSocket35200–
Webhook endpoints––20–
Custom domains––10–
API keys––10–

A dash means the feature is not offered at that tier, or is not reachable with that credential: the WebSocket accepts session tokens only, and webhooks, custom domains and API keys are managed from a logged-in session.

Requests without credentials (anon_ip) get 30 requests per minute (burst 10) and 5 availability checks per minute. They cannot own inboxes. Login is limited to 10 attempts per hour per caller and also per account code. The SMTP receiver refuses any single message larger than 25 MiB, whatever the tier.

These numbers can change. GET /v1/account returns the resolved limits object for your account. In that object, an optional quota is null when the feature is not offered at your tier.

Limits for a logged-in account are counted per account, across all of its sessions. Registration, login, inbox creation and availability checks are also counted per client network (an IPv4 address, or an IPv6 /64) with the same numbers. Creating extra sessions does not buy more of them.


Rate limits and quotas

  • Name
    429 rate_limited
    Description

    A rate limit is used up. The response carries Retry-After in seconds. Wait at least that long. Retrying sooner does not help. Every request also counts against a per-IP budget of 600 requests per minute, checked before authentication.

  • Name
    402 payment_required
    Description

    A count quota is full (live inboxes, API keys, webhooks, custom domains), or there is not enough credit for a purchase. Delete something, or upgrade. The message says which limit was hit.

  • Name
    403 tier_required
    Description

    The feature is not offered at your tier, for example search for an anonymous session or webhooks for a free account.

Responses that pass the tier limit carry X-RateLimit-Limit, the per-minute request limit of your tier.


Errors

Every error has the same body: an error object with a stable, machine-readable code and a human-readable message.

Error body

{
  "error": {
    "code": "unauthorized",
    "message": "authentication required"
  }
}
StatuscodeMeaning
400bad_requestThe body, a query parameter or a value is invalid. The message names the problem.
401unauthorizedNo credentials, or the token is unknown, expired or revoked. Also returned for an anonymous session on an endpoint that needs an account.
401invalid_credentialsA login or password check failed. Unknown account code, wrong password and wrong recovery key all get this same answer.
402payment_requiredA count quota is full, or credit is short.
403tier_requiredThe feature is not available at your tier.
403forbiddenAuthenticated but not allowed, for example an API key without the scope for this method.
404not_foundThe resource does not exist or is not yours.
409conflictThe address or name is taken or reserved, or the session is already logged in.
429rate_limitedA rate limit is used up. See Retry-After.
500internalServer error. No detail is returned.
503not_readyA server dependency is not ready. Retry later.
503unavailableA feature's backing service is not configured or not reachable (payments, paste passes).

Pagination

Lists that can grow without bound (an inbox's messages, the credit ledger) use cursor pagination.

  • Name
    limit
    Type
    integer
    Description

    Page size. Default 25, clamped to 1–100.

  • Name
    cursor
    Type
    string
    Description

    Opaque cursor from the previous page's next_cursor.

A page is an object with items (newest first) and next_cursor. next_cursor is null on the last page. A malformed cursor returns 400 bad_request. Other lists (inboxes, webhooks, API keys, deposits) are returned whole, as JSON arrays.

Page

{
  "items": [],
  "next_cursor": null
}

CORS

The API allows browser requests from other origins only if the origin is on the server's list. Today the list has one entry, https://paste.nullnet.sh. The mail web app is served from the same origin as the API and needs no CORS. CORS does not affect server-side clients.

Allowed methods are GET, POST, PATCH, DELETE and OPTIONS. Allowed request headers are Authorization, Content-Type and X-API-Key. The API uses bearer tokens only. It sets no cookies, and CORS never allows credentials.


GET/v1/domains

List domains

The system domains you can create addresses on. No authentication. Premium accounts can also add their own domains; see custom domains.

Response

An array of objects.

  • Name
    name
    Type
    string
    Description

    Domain name.

Request

GET
/v1/domains
curl https://mail.nullnet.sh/v1/domains

Response

[
  { "name": "mail.nullnet.sh" },
  { "name": "nullnet.sh" }
]

Quick start

Create an inbox and read it

# 1. Anonymous session
TOKEN=$(curl -s -X POST https://mail.nullnet.sh/v1/session | jq -r .token)

# 2. Inbox with a random address
INBOX=$(curl -s -X POST https://mail.nullnet.sh/v1/inboxes \
  -H "Authorization: Bearer $TOKEN")
echo "$INBOX" | jq -r .address    # e.g. kqv-7f3m2a@mail.nullnet.sh

# 3. Send mail to that address, then list it
curl -s https://mail.nullnet.sh/v1/inboxes/$(echo "$INBOX" | jq -r .id)/messages \
  -H "Authorization: Bearer $TOKEN"

Next: Accounts and sessions, Inboxes, Messages, and the WebSocket for live updates.