Accounts and sessions
Every caller that owns inboxes holds a session token. A session starts anonymous. You can turn it into an account by registering, or log it in to an existing account. An account is identified only by a random account code and protected by a password. No email address or other contact detail is ever asked for.
Account codes and recovery keys
- The account code is 16 Crockford base32 symbols, shown as
XXXX-XXXX-XXXX-XXXX. The server generates it. - The recovery key is 39 Crockford base32 symbols, shown in groups of five. It can log in instead of the password.
- Both are returned once, in the registration response. The server stores only hashes of them. There is no email on file and no reset flow: if you lose the code, or lose both the password and the recovery key, the account cannot be recovered.
- On input, codes and keys may use any case and may include or leave out the dashes.
IandLare read as1, andOas0. - Passwords are 12 to 128 characters and must not be on a list of common passwords.
Responses other than registration show the code masked, as ****-****-****-XXXX.
Create an anonymous session
Creates an anonymous session and returns its token. The token appears only in this response. No credentials are needed and the body is optional.
Optional attributes
- Name
referral_code- Type
- string
- Description
A referral code to attribute to, if this session later registers an account. See Referrals.
Response
- Name
token- Type
- string
- Description
Bearer token. Store it now.
- Name
expires_at- Type
- timestamp
- Description
Session expiry. It moves forward while the session is used (30 days from last use).
- Name
tier- Type
- string
- Description
anon_session.
Request
curl -X POST https://mail.nullnet.sh/v1/session
Response
{
"token": "8lJ0vD2h6kq0Q8m7tX3yZr1cB5nW4eA9sUfGpKoLiHM",
"expires_at": "2026-11-25T08:00:00Z",
"tier": "anon_session"
}
Get the current session
Returns the session the token belongs to.
Response
- Name
id- Type
- string
- Description
Session id.
- Name
expires_at- Type
- timestamp
- Description
Session expiry.
- Name
tier- Type
- string
- Description
anon_session,free_accountorpremium_account.
- Name
has_account- Type
- boolean
- Description
Whether the session is logged in to an account.
Request
curl https://mail.nullnet.sh/v1/session \
-H "Authorization: Bearer {token}"
Response
{
"id": "019a2f4e-6b1c-7d20-9a3e-5c8f1b2d4e6a",
"expires_at": "2026-11-25T08:00:00Z",
"tier": "anon_session",
"has_account": false
}
End the session
Revokes the token. Inboxes owned by an anonymous session keep running until they expire, but nobody can reach them any more. Returns 204.
Request
curl -X DELETE https://mail.nullnet.sh/v1/session \
-H "Authorization: Bearer {token}"
Register an account
Creates an account and returns its account code and recovery key, once.
- With an anonymous session token, that session is promoted. It becomes logged in, keeps its token (
tokenisnullin the response), and its inboxes move to the account, up to the account's live-inbox limit.adopted_inboxessays how many moved. - Without a token, a new logged-in session is created and its token is returned.
Returns 201. Returns 409 conflict if the session is already logged in. Registration is rate limited per caller and per client network (5 per hour for anonymous callers).
Required attributes
- Name
password- Type
- string
- Description
12–128 characters, not a common password.
Optional attributes
- Name
referral_code- Type
- string
- Description
Used only when no session token is sent. A promoted session uses the code it was created with.
Response
- Name
account_code- Type
- string
- Description
XXXX-XXXX-XXXX-XXXX. Shown only here.
- Name
recovery_key- Type
- string
- Description
Grouped recovery key. Shown only here.
- Name
token- Type
- string | null
- Description
New bearer token, or
nullwhen the presenting session was promoted (keep using that token).
- Name
expires_at- Type
- timestamp
- Description
Session expiry.
- Name
tier- Type
- string
- Description
free_accountfor a new account.
- Name
adopted_inboxes- Type
- integer
- Description
Inboxes moved from the anonymous session into the account.
- Name
warning- Type
- string
- Description
A reminder that the code and key cannot be recovered.
Request
curl https://mail.nullnet.sh/v1/accounts \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{"password": "correct horse battery staple"}'
Response
{
"account_code": "7KQ2-M9XH-4TPB-R3DZ",
"recovery_key": "5H8QZ-2MV7K-XR4TA-9PB3W-6NCJE-1DFGY-0SKMQ-7HXA",
"token": null,
"expires_at": "2026-11-25T08:00:00Z",
"tier": "free_account",
"adopted_inboxes": 2,
"warning": "Store the account code and recovery key now. There is no email on file and no reset flow: if both are lost, the account cannot be recovered."
}
Log in
Logs in with the account code and exactly one secret: the password or the recovery key.
If you send an anonymous session token, that session is promoted like at registration: it keeps its token and its inboxes move to the account. Otherwise a new token is returned.
An unknown account code, a wrong password and a wrong recovery key all return the same 401 invalid_credentials, in the same time. Attempts are limited to 10 per hour per caller and 10 per hour per account code.
Required attributes
- Name
account_code- Type
- string
- Description
The account code, in any accepted spelling.
Exactly one of
- Name
password- Type
- string
- Description
The password.
- Name
recovery_key- Type
- string
- Description
The recovery key, instead of the password.
Response
- Name
token- Type
- string
- Description
Bearer token: the one you sent if an anonymous session was promoted, otherwise a new one.
- Name
expires_at- Type
- timestamp
- Description
Session expiry.
- Name
tier- Type
- string
- Description
free_accountorpremium_account.
- Name
account_code- Type
- string
- Description
Masked account code.
- Name
adopted_inboxes- Type
- integer
- Description
Inboxes moved from the anonymous session into the account.
Request
curl https://mail.nullnet.sh/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"account_code": "7kq2m9xh4tpbr3dz", "password": "correct horse battery staple"}'
Response
{
"token": "q3Zt9WbV1xYc0kE7rN2mH5uJ8fA4sDgPoLiK6aMnBvC",
"expires_at": "2026-11-25T08:00:00Z",
"tier": "free_account",
"account_code": "****-****-****-R3DZ",
"adopted_inboxes": 0
}
Log out
Revokes the current session. Returns 204. The account's inboxes are not affected.
Request
curl -X POST https://mail.nullnet.sh/v1/auth/logout \
-H "Authorization: Bearer {token}"
Log out everywhere
Revokes every session of the account, including this one. Takes effect at once. Returns 204. Needs a logged-in session.
Request
curl -X POST https://mail.nullnet.sh/v1/auth/logout-all \
-H "Authorization: Bearer {token}"
Get the account
Returns the logged-in account and the limits that apply to it. Use limits to show quotas in a UI instead of hard-coding them.
Response
- Name
account_code- Type
- string
- Description
Masked code,
****-****-****-XXXX.
- Name
tier- Type
- string
- Description
free_accountorpremium_account.
- Name
premium- Type
- boolean
- Description
Whether a paid plan is active.
- Name
created_at- Type
- timestamp
- Description
Registration time.
- Name
inboxes- Type
- integer
- Description
Live inboxes owned by the account.
- Name
limits- Type
- object
- Description
The resolved limits for this account. Field names match the limits table (
concurrent_inboxes,inbox_retention_secs,messages_per_inbox,storage_bytes_per_inbox,max_message_bytes,search_per_min,ws_connections,webhooks,custom_domains,api_keys, and more). An optional quota isnullwhen the feature is not offered.limits.pasteholds the limits signed into paste passes.
Request
curl https://mail.nullnet.sh/v1/account \
-H "Authorization: Bearer {token}"
Response
{
"account_code": "****-****-****-R3DZ",
"tier": "free_account",
"premium": false,
"created_at": "2026-10-26T08:00:00Z",
"inboxes": 2,
"limits": {
"api_requests_per_min": 120,
"api_burst": 40,
"inbox_create_per_hour": 30,
"concurrent_inboxes": 5,
"inbox_retention_secs": 86400,
"messages_per_inbox": 200,
"storage_bytes_per_inbox": 104857600,
"max_message_bytes": 10485760,
"search_per_min": 10,
"ws_connections": 3,
"ws_subscriptions_per_conn": 5,
"webhooks": null,
"custom_domains": null,
"api_keys": null,
"paste": {
"max_size_bytes": 1048576,
"max_ttl_secs": 86400,
"max_files": 1,
"live_storage_bytes": null,
"uploads_per_min": 10,
"dashboard": false,
"open_alerts": false,
"file_requests": false
}
// ...
}
}
Change credentials
Changes the password, issues a new recovery key, or both. The current password is always required. A wrong one returns 401 invalid_credentials.
A password change logs out every other session of the account. A new recovery key replaces the old one, which stops working, and is shown only in this response.
Required attributes
- Name
current_password- Type
- string
- Description
The current password.
Optional attributes (at least one)
- Name
new_password- Type
- string
- Description
The new password (12–128 characters, not a common password).
- Name
rotate_recovery_key- Type
- boolean
- Description
Issue a new recovery key. Default
false.
Response
- Name
password_changed- Type
- boolean
- Description
Whether the password changed.
- Name
recovery_key- Type
- string | null
- Description
The new recovery key, shown once, or
null.
- Name
warning- Type
- string | null
- Description
Loss warning when a key was issued.
Request
curl -X PATCH https://mail.nullnet.sh/v1/account \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{"current_password": "correct horse battery staple", "rotate_recovery_key": true}'
Response
{
"password_changed": false,
"recovery_key": "9DXM4-QW2KT-7HPAR-3BZN8-5CFJE-0GYVS-1MKTQ-6RXA",
"warning": "Store the account code and recovery key now. There is no email on file and no reset flow: if both are lost, the account cannot be recovered."
}
Delete the account
Deletes the account at once. There is no grace period. Every inbox is deleted (its mail is purged shortly after), every session and API key is revoked, custom domains are removed, and both credentials are destroyed. Returns 204.
Required attributes
- Name
password- Type
- string
- Description
The current password.
Request
curl -X DELETE https://mail.nullnet.sh/v1/account \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{"password": "correct horse battery staple"}'
Referrals
Every account has a public referral code: 4–16 lowercase letters and digits, safe to share. Generated codes are 8 characters. The share link is https://mail.nullnet.sh/r/<code>.
A referral is attributed once, when the referred account is created, and only for an active code of another live account. Pass the code as referral_code to POST /v1/session or POST /v1/accounts. A bad or unknown code never fails the signup; it is ignored.
The referrer earns 5 % of the referred account's crypto deposits as credit, for 12 months after the signup. A reward is paid only if the referrer has already deposited at least $5 themselves; a referee deposit credited before that earns nothing. One referred account can earn a referrer at most $50 in total. Rewards appear in the ledger as referral_reward. Referral stats show totals only, never a list of referred accounts.
Get referral stats
Returns your code, share link and totals. Needs a logged-in session.
Response
- Name
code- Type
- string | null
- Description
Active referral code.
- Name
share_url- Type
- string | null
- Description
https://mail.nullnet.sh/r/<code>.
- Name
signups- Type
- integer
- Description
Accounts attributed to you.
- Name
deposited_referees- Type
- integer
- Description
How many of those have deposited.
- Name
earned- Type
- string
- Description
Reward credit paid so far, in USD.
- Name
pending- Type
- string
- Description
Reward credit awaiting release, in USD.
- Name
reward_bps- Type
- integer | null
- Description
Your reward rate in basis points of referee deposits (
500= 5 %).
- Name
window_months- Type
- integer
- Description
How long after signup a referee's deposits earn rewards.
Request
curl https://mail.nullnet.sh/v1/referrals \
-H "Authorization: Bearer {token}"
Response
{
"code": "k3v9qz2m",
"share_url": "https://mail.nullnet.sh/r/k3v9qz2m",
"signups": 4,
"deposited_referees": 1,
"earned": "0.500000000000000000",
"pending": "0.000000000000000000",
"reward_bps": 500,
"window_months": 12
}
Rotate the referral code
Replaces your code with a new random one. The old code stops attributing at once. Rate limited per tier. Returns the same object as GET /v1/referrals.
Request
curl -X POST https://mail.nullnet.sh/v1/referrals/rotate \
-H "Authorization: Bearer {token}"
Choose a vanity code
Sets a code of your choice. Premium accounts only; other tiers get 403 tier_required. The code is case-insensitive, 4–16 letters and digits, and must not be a reserved word such as admin or support. Returns 409 conflict if the code is taken. The old code stops attributing. Returns the same object as GET /v1/referrals.
Required attributes
- Name
code- Type
- string
- Description
The code you want.
Request
curl -X PUT https://mail.nullnet.sh/v1/referrals/code \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d '{"code": "alice"}'