TmpMails docs

API reference

Base URL https://api.tmpmails.net/api/v1. Everything is JSON; send Accept: application/json and, where a body is required, Content-Type: application/json.

67 operations across 15 groups, taken straight from the contract.

account

GET /me

Current account

Auth: userToken

Responses: 200 401

PATCH /me

Update the account

Auth: userToken

FieldTypeRules
localestringoptional, max 10

Responses: 200 401 422

GET /me/2fa

Two-factor status

Auth: userToken

Responses: 200 401

DELETE /me/2fa

Disable two-factor

Requires a current code, so a stolen session alone cannot remove the second factor.

Auth: userToken

FieldTypeRules
codestringrequired, max 16

Responses: 204 401 422

POST /me/2fa/setup

Begin two-factor enrolment

The secret is held server-side for a short window and is not active until `confirm` succeeds, so an abandoned enrolment cannot lock the account out.

Auth: userToken

Responses: 200 401 409

POST /me/2fa/confirm

Activate two-factor

Auth: userToken

FieldTypeRules
codestringrequired, max 16

Responses: 200 401 422

auth

POST /auth/password/forgot

Ask for a password reset link

Always answers 204, whether or not the address has an account. Answering differently would turn this into a way to find out who has one.

Auth: public

FieldTypeRules
emailstringrequired, max 255

Responses: 204 422 429

POST /auth/password/reset

Set a new password with a reset token

The token is single use and expires after 30 minutes. Two-factor enrolment is untouched: a reset recovers the password, not the second factor.

Auth: public

FieldTypeRules
tokenstringrequired, min 32, max 128
passwordstringrequired, min 8, max 200

Responses: 204 410 422 429

POST /auth/token

Sign in

With 2FA enabled the first call answers `401 auth.totp_required`; repeat it with `code`. Invalid credentials and a wrong code are the same failure to the caller by design.

Auth: public

FieldTypeRules
emailstringrequired, max 255
passwordstringrequired, max 200
codestringoptional, max 16

Responses: 200 401 422 429

billing

GET /plans

Plan and add-on catalog (prices in points)

Auth: public

Responses: 200

GET /payment-providers

Payment gateway registry (public)

Mirrors `settings['billing.providers']` so the frontend renders a switched-off gateway as a disabled button instead of hiding it or letting it be clicked until the next deploy.

Auth: public

Responses: 200

GET /me/subscription

Current subscription and effective entitlements

Auth: userToken

Responses: 200 401

POST /me/subscription

Subscribe or switch plan (debits points immediately)

Debit and activation happen in one transaction. Switching plans cancels the previous subscription and starts a fresh period; no proration.

Auth: userToken

FieldTypeRules
planCodestringrequired
intervalstringrequired, one of monthly, yearly

Responses: 201 401 402 422

DELETE /me/subscription

Turn off auto-renew (the paid period is never cut short)

Auth: userToken

Responses: 200 401

POST /me/addons

Buy an add-on (debits points immediately, keeps the current plan)

An add-on is bought on top of the running plan, never instead of it. Buying the same add-on again stacks a second subscription and raises the entitlement again.

Auth: userToken

FieldTypeRules
codestringrequired
intervalstringrequired, one of monthly, yearly

Responses: 201 401 402 422

GET /me/wallet

Wallet balance

Auth: userToken

Responses: 200 401

GET /me/wallet/transactions

Points ledger (paginated)

Auth: userToken

ParameterInType

Responses: 200

GET /me/wallet/topups

List the caller's top-up orders

A pending order carries the only copy of its bank memo. Without this list a customer who closed the tab has no way back to the account number they still have to pay.

Auth: userToken

ParameterInType
limitqueryinteger

Responses: 200

POST /me/wallet/topups

Create a top-up order

`sepay` is active (VND bank transfer). `bnb` is `coming_soon` and returns 400 payment.provider_unavailable. The wallet is never credited from the bank webhook directly — reconciliation matches memo + amount.

Auth: userToken

FieldTypeRules
providerstringrequired, one of sepay, bnb
amountintegerrequired, ≥ 1
currencystringrequired, one of VND

Responses: 201 400 422

GET /me/wallet/topups/{orderId}

Poll top-up order status

Auth: userToken

ParameterInType
orderIdpathstring

Responses: 200 404

content

GET /content/notices

Notices to show over the product

Banners and dialogs published by staff, already filtered by schedule. The HTML is sanitised on write against a fixed allowlist, so it is safe to render as-is.

Auth: public

Responses: 200

GET /content/pages

Published pages

Titles and keys only — enough for a footer, without shipping every page body.

Auth: public

Responses: 200

GET /content/footer

Staff-written site footer for the requested language

`null` when nobody has published a footer, which is not an error — the client then renders its built-in footer. Falls back to the default locale when the requested one has no translation yet.

Auth: public

Responses: 200

GET /content/pages/{key}

One published page

Auth: public

ParameterInType
keypathstring

Responses: 200 404

domains

GET /me/domains

List private domains

Auth: userToken

Responses: 200 401

POST /me/domains

Add a private domain

Auth: userToken

FieldTypeRules
domainstringrequired, max 253

Responses: 201 401 403 409 422

DELETE /me/domains/{id}

Remove a private domain

Auth: userToken

Responses: 204 401 404

PATCH /me/domains/{id}/catch-all

Turn catch-all on or off for an own domain

With catch-all on, mail to any local part of the domain is accepted and a mailbox is created on first delivery. Requires the `domains.catchAll` entitlement and a verified domain.

Auth: userToken

FieldTypeRules
enabledbooleanrequired

Responses: 200 401 403 404 422

POST /me/domains/{id}/verify

Re-check the DNS records

Both the TXT proof and the MX record must resolve. A domain that verified once is re-checked periodically, so removing the MX later suspends it again.

Auth: userToken

Responses: 200 401 404 422

filters

GET /me/filters

List custom extraction filters

Auth: userToken

Responses: 200 401

POST /me/filters

Create a filter (entitlement `filters.max`)

Auth: userToken

FieldTypeRules
namestringrequired, max 100
scopeTypestringoptional, one of all, domain
domainIdstring,nulloptional
fromPatternstring,nulloptional
subjectPatternstring,nulloptional
extractRegexstringrequired, max 512
priorityintegeroptional, ≥ 1, ≤ 9999
statusstringoptional, one of active, disabled

Responses: 201 401 422

POST /me/filters/test

Dry-run a filter against a sample without saving

Auth: userToken

FieldTypeRules
filterextractRegex, fromPattern, subjectPatternrequired
samplefrom, subject, textrequired

Responses: 200 401 422

PATCH /me/filters/{id}

Update a filter

Auth: userToken

FieldTypeRules
namestringoptional, max 100
extractRegexstringoptional, max 512
fromPatternstring,nulloptional
subjectPatternstring,nulloptional
priorityintegeroptional
statusstringoptional, one of active, disabled

Responses: 200 401 422

DELETE /me/filters/{id}

Delete a filter

Auth: userToken

Responses: 204 401

integrations

POST /integrations/alerts

Alertmanager webhook — delivers operational alerts to the configured chat

Not part of the product surface. Delivery lives here rather than in Alertmanager because Alertmanager's Telegram client logs the URL it failed to call, and that URL contains the bot token. Authenticated by a shared secret, not a user session: the caller is a container. Always answers 200 once authorised — a non-2xx only makes Alertmanager retry a batch that will not format any better the second time.

Auth: alertWebhook

FieldTypeRules
statusstringoptional, one of firing, resolved
commonLabelsalertname, severityoptional
alertsannotations[]optional

Responses: 200 401

POST /integrations/sepay/webhook

SePay bank-transaction callback (append-only, idempotent)

Guarded by `Authorization: Apikey <shared-secret>` plus an ingress IP allowlist. The handler only appends a `sepay_transactions` row; crediting happens in the reconciliation job.

Auth: sepayApikey

Responses: 200 401

keys

GET /me/api-keys

List developer keys

Auth: userToken

Responses: 200 401

POST /me/api-keys

Issue a developer key

`key` is returned exactly once — only its hash is stored, so a database leak cannot be replayed. The tier follows the account plan and cannot be chosen.

Auth: userToken

FieldTypeRules
namestringrequired, max 100
scopesstring[]required
allowedCidrsstring[]optional
expiresAtstringoptional

Responses: 201 401 403 422

DELETE /me/api-keys/{prefix}

Revoke a developer key

Auth: userToken

Responses: 204 401 404

POST /keys/mailboxes

Create a mailbox with a developer key

Requires scope `mailboxes:write`. The mailbox belongs to the key's owner.

Auth: apiKey

FieldTypeRules
domainstringoptional, max 255
localPartstringoptional, max 64
ttlintegeroptional, ≥ 60

Responses: 201 401 403 422 429

GET /keys/mailboxes/{id}/messages

List a mailbox's messages with a developer key

Requires scope `messages:read`. A mailbox the key does not own answers 404 rather than 403, so the endpoint cannot be used to discover which mailboxes exist.

Auth: apiKey

ParameterInType

Responses: 200 401 403 404

GET /keys/mailboxes/{id}/otp/latest

Latest OTP for a mailbox with a developer key

Requires scope `otp:read`.

Auth: apiKey

Responses: 200 401 403 404

mailboxes

GET /domains

Active public domains

Auth: public

Responses: 200

GET /me/mailboxes

Mailboxes this account owns

Newest first, each with a bearer token. This is the only way to find a catch-all mailbox: it was opened by an incoming message, so the owner has never seen the address.

Auth: userToken

Responses: 200 401

POST /mailboxes

Create a mailbox

Auth: public

ParameterInType
FieldTypeRules
domainstringoptional
localPartstringoptional, min 1, max 64, pattern ^[a-z0-9][a-z0-9._-]{0,63}$
ttlintegeroptional, ≥ 60

Responses: 201 409 422 429

GET /mailboxes/me

Mailbox info + TTL

Auth: mailboxToken

Responses: 200 401 410

DELETE /mailboxes/me

Destroy the mailbox now

Auth: mailboxToken

Responses: 204 401

POST /mailboxes/me/extend

Extend mailbox TTL (re-issues the mailbox token)

Auth: mailboxToken

FieldTypeRules
ttlintegeroptional, ≥ 60

Responses: 200 403 410

messages

GET /mailboxes/me/messages

List messages (cursor paginated, optional long-poll)

Auth: mailboxToken

ParameterInType

Responses: 200 401 410

GET /messages/{id}

Full message (sanitized HTML)

Auth: mailboxToken

Responses: 200 404

DELETE /messages/{id}

Delete a message

Auth: mailboxToken

Responses: 204 404

GET /messages/{id}/raw

Download the original message (.eml)

The bytes as delivered, before parsing — full `Received:` chain, `Authentication-Results`, the untouched MIME tree and the original HTML before sanitizing. Opens in any mail client. The original is the largest per-message item in redis-mail, which runs `noeviction`, so it is kept only briefly: **15 minutes** for free mailboxes and **1 hour** for paid ones. After that this answers `410 message.raw_expired` — gone, not missing. `Message.rawExpiresAt` says when, so a client can drop the button rather than offer a download that fails.

Auth: mailboxToken

Responses: 200 404 410

GET /messages/{id}/attachments/{attId}

Download an attachment

Always served as `application/octet-stream` with `Content-Disposition: attachment` and `nosniff`, whatever the sender declared: the file arrived from an unauthenticated stranger, so the browser must never be allowed to render it. Attachments above the inline size limit are not retained and answer 404.

Auth: mailboxToken

ParameterInType
attIdpathstring

Responses: 200 404

otp

GET /mailboxes/me/otp/latest

Latest extracted OTP

Auth: mailboxToken

ParameterInType

Responses: 200 404

session

GET /session

Mailboxes opened in this session

Newest first. Expired mailboxes are omitted rather than listed as dead addresses. Each mailbox carries a freshly minted bearer token capped at its own remaining TTL — the guest key is the only credential the visitor kept, so without tokens the list would name addresses it cannot read.

Auth: guestSession

Responses: 200 401

POST /session

Open a guest session

Redis-only, sliding 30 minutes, no account. Lets an anonymous visitor keep several mailboxes across reloads. It carries no plan and grants no extra quota — mailbox creation stays clamped per IP, so minting a new session buys nothing.

Auth: public

Responses: 201 429

DELETE /session

End the guest session

Drops the key and the session's mailbox index. The mailboxes themselves keep their own TTL — this is signing out, not deleting mail the visitor may still hold a token for.

Auth: guestSession

Responses: 204 401

site

GET /site

Title, description and indexing rules for the hostname being read

Resolved from the request's Host header, not a parameter: this is per-site configuration, and letting a caller name a different site would hand out the wrong canonical URL.

Auth: public

Responses: 200

GET /site/robots.txt

robots.txt for the hostname being read

Auth: public

Responses: 200

GET /site/sitemap.xml

sitemap.xml for the hostname being read

Auth: public

Responses: 200

system

GET /healthz

Liveness probe

Auth: public

Responses: 200

GET /readyz

Readiness probe (dependencies checked)

Auth: public

Responses: 200 503

webhooks

GET /me/webhooks

List webhook endpoints

Auth: userToken

Responses: 200 401

POST /me/webhooks

Register an endpoint (entitlement `webhooks.max`)

The URL must be https on port 443/8443 and resolve to a public address. The response is the only time the signing secret is returned.

Auth: userToken

FieldTypeRules
urlstringrequired, max 2048
eventsstring[]required
domainIdstring,nulloptional

Responses: 201 401 403 422

PATCH /me/webhooks/{id}

Update an endpoint (resuming clears the failure history)

Auth: userToken

FieldTypeRules
urlstringoptional
eventsstring[]optional
statusstringoptional, one of active, paused

Responses: 200 401 422

DELETE /me/webhooks/{id}

Delete an endpoint

Auth: userToken

Responses: 204 401

GET /me/webhooks/{id}/deliveries

Recent delivery attempts (ring buffer, last 100)

Auth: userToken

ParameterInType
idpathstring

Responses: 200 401