?docs · the field guide

The field guide.

From your first disposable address to a fully self-hosted deployment on Heroku and GitHub Pages — here’s how this mail drop works, and how to run your own.

updated 20268 sectionsmit licensed

01 · Workflow

How to use

No account, no sign-up. Head to the pantry, jam a tag in or leave it blank, and hit Print an address — you get a disposable {namespace}.{tag}@inbox.testmail.app inbox in one click.

1 · Print an address

Generate a fresh address from the hero card or the dashboard’s New Address button. It copies to your clipboard automatically, and the curl snippet is right there to paste into your scripts.

2 · Send mail to it

Use it anywhere a form, sign-up, or test flow asks for an email. Mail sent to name.tag@inbox.testmail.app lands in your live inbox.

3 · Watch it arrive

The delivery log streams live over SSE — new mail posts itself to the list without a refresh. Filter by tag, search sender or subject, or flip between All / Unread.

4 · Read it safely

Open any message in the built-in viewer: a sanitized HTML iframe, a plain-text tab, and raw headers. Spam score is graded with a rule-by-rule SpamAssassin breakdown. Attachments and .eml download through a proxy.

Everything is disposable — generate as many addresses as you need and throw the inbox away when you’re done. Prefix your tags (signup.flow, otp.test) so later tests are easy to filter.

02 · Offline

Run it locally

Clone the repo, spin up the FastAPI gateway, point the Vite dev server at it, and you’re offline on localhost:5173. Without API keys the backend happily serves mock emails if you turn on demo mode.

backend
cd backend
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env      # add your testmail.app keys (see "Set up testmail.app")
python run.py             # API on http://localhost:8000 — swagger at /docs
frontend
cd frontend
pnpm install              # or npm install
cp .env.production .env   # VITE_API_URL=http://localhost:8000 for a local backend
pnpm dev                  # UI on http://localhost:5173

Demo mode (no keys, mock emails)

Set DEBUG=true and ALLOW_DEMO_MODE=true (or drop both in backend/.env) to get sample emails with no external account. Great for frontend work without burning a quota.

03 · Self-host

Backend on Heroku

The backend is a single FastAPI service, shipped as a Docker image. The included heroku.yml tells Heroku which Dockerfile to build, so deployment is one command.

deploy: heroku
heroku create testmail-api --region eu --stack container
heroku stack:set container
git push heroku main

1 · Create the app, then push. Heroku builds backend/Dockerfile and starts uvicorn on the port it assigns.

2 · Add secrets under Settings → Reveal Config Vars: API_KEY and NAMESPACE (from testmail.app). Set CORS_ORIGINS to your frontend origin.

3 · In the Resources tab set the dyno type to Basic — it is the smallest always-on tier. The Eco tier is cheaper but sleeps after 30 minutes idle, and all Eco dynos share one pool of hours, so a second app can exhaust it.

4 · Smoke test: curl https://YOUR-APP.herokuapp.com/health and generate an address with curl -X POST https://YOUR-APP.herokuapp.com/api/emails/generate.

Note on demo mode in production

Keep ALLOW_DEMO_MODE=false (the default) unless you intend to serve mock emails — and note it requires DEBUG=true for safety, so it is really only for local work.

04 · Self-host

Frontend on Vercel

The production site is a static Astro build served by Vercel. Every push tomaintriggers pnpm build, which runs astro buildand then regenerates sitemap.xml from the pages that were just built.

steps
1. Fork or clone the repo.
2. Import the repository in Vercel and set Root Directory to frontend.
3. Add PUBLIC_API_URL (or VITE_API_URL) pointing at your backend.
4. Deploy. Vercel runs pnpm build and serves dist/.
5. Add your domain in Vercel, then point DNS at the Vercel target.

Keep secrets out of the frontend build

Only PUBLIC_API_URL belongs in the frontend build. Anything prefixedPUBLIC_ orVITE_ is inlined into the public bundle, so the admin key belongs on the backend host as ADMIN_PASSWORD — set it in the host’s environment, never in a frontend build secret.

Canonical URLs · Vercel is configured with trailingSlash: false, so /blog/post/ redirects to /blog/post and every page has one indexable URL.

Sitemap · The build scans dist/ for indexable pages. A new post is added automatically; drafts, noindex pages, and redirect stubs are skipped.

Network · Make sure your backend’s CORS_ORIGINS includes your Vercel domain, or the browser will block the API calls.

05 · Account

Set up testmail.app

testmail.app is the service that actually receives and stores the mail. You point one API key and one namespace at it — the gateway does the rest.

1 · Create an account · Sign up at testmail.app (GitHub or email). If you’re a student, claim the free Essential tier through the GitHub Student Developer Pack — it’s a partner perk worth $10/month.

2 · Copy your API key · In the testmail.app console, go to Secrets → API keys and copy a token. This is your TESTMAIL_API_KEY.

3 · Find your namespace · Your namespace is the mailbox name testmail.app gives you (e.g. the account handle shown in the console). It’s the part before the dot in every address: {namespace}.{tag}@inbox.testmail.app. This is your TESTMAIL_NAMESPACE.

4 · Mailbox type · testmail.app auto-opens any mailbox on first use. Keep the namespace in sync between the console and your env var — that’s the only tricky bit.

raw testmail.app sanity check
curl "https://api.testmail.app/api/json?apikey=YOUR_API_KEY&namespace=YOUR_NAMESPACE&tag=test&limit=3<&callback="

Once your backend has keys configured, the app leaves demo mode automatically — hitting /health should report api_key_configured: true.

06 · Reference

Environment variables

Everything is plain environment variables — no config files to drift.

Backend

VariableDescriptionDefault
TESTMAIL_API_KEYYour testmail.app API key (Secrets → API keys → copy)"" → demo mode
TESTMAIL_NAMESPACEYour testmail.app mailbox / namespace name"" → demo mode
API_KEYLegacy alias for TESTMAIL_API_KEY—
NAMESPACELegacy alias for TESTMAIL_NAMESPACE—
BASE_URLtestmail.app API basehttps://api.testmail.app/api
CORS_ORIGINSComma-separated origins allowed to call the APIlocalhost:5173 + codaipro.com
DEBUGEnable debug mode (required for ALLOW_DEMO_MODE)false
ALLOW_DEMO_MODEServe mock emails when no credentials are setfalse
EMAIL_RETENTION_MINUTESAuto-hide retention window in minutes (older emails purged)10
ADMIN_PASSWORDAdmin access key: console login, archive mode, telemetry feed (no default)required in prod
SELF_PING_URLYour service URL (keep-alive ping target, idle-shutdown hosts only)null
SELF_PING_ENABLEDPing yourself to avoid free-tier spin-downfalse
PING_INTERVAL_MINUTESMinutes between keep-alive pings10
TRUSTED_PROXY_NETWORKSCIDRs whose forwarded client IP may be trusted (empty = trust nobody)
ATTACHMENT_MAX_BYTESCap for a proxied attachment or .eml download10000000
TESTER_ALLOWED_RECIPIENT_DOMAINSDomains the anonymous email tester may deliver toinbox.testmail.app

Frontend (baked at build time)

VariableDescriptionDefault
VITE_API_URLBackend base URL your build talks tohttps://api-testmail.codaipro.com

In CI these come from repository secrets with sensible fallbacks — see the workflow file for the exact wiring.

07 · Wire

API reference

Every endpoint the UI uses, directly scriptable. Swap the host for your own backend URL.

POST/api/emails/generateGenerate a disposable `{namespace}.{tag}@inbox.testmail.app` address
GET/api/emails/List/poll emails with filters, tags and pagination
GET/api/emails/{id}Full detail: html, text, headers, spam report, attachments
GET/api/emails/{id}/linksExtract unique URLs from the email
GET/api/emails/{id}/downloadDownload the raw email as RFC 822 .eml
GET/api/emails/attachments/{id}/{filename}Securely proxy an attachment download
GET/api/emails/streamServer-Sent Events stream (live inbox)
GET/api/tagsUnique tags with counts (alias: /api/emails/tags)
GET/health, /pingHealth + keep-alive endpoints

Headers · Credentials travel in headers, never in the query string — a URL ends up in access logs, proxy logs and browser history.

X-Admin-KeyAdmin key: console login, unlimited archive, telemetry, private mailboxes
X-Mailbox-KeyRecovery key for one private mailbox — sent as a header, never as a query parameter
Authorization: Bearer tm_live_…Developer API key, subject to the key’s own request budget
generate an address
curl -X POST "https://api-testmail.codaipro.com/api/emails/generate?tag=release" \
  -H "Content-Type: application/json"
# → {"email":"yourns.release@inbox.testmail.app","tag":"release","namespace":"yourns"}
list, filter and live-poll
# latest mail
curl "https://api-testmail.codaipro.com/api/emails/?limit=20"

# wait up to 60s for the next incoming email
curl "https://api-testmail.codaipro.com/api/emails/?livequery=true&timestamp_from=$(( $(date +%s) * 1000 ))"

# filter by tag / sender / subject, with attachments only
curl "https://api-testmail.codaipro.com/api/emails/?tag=otp&has_attachments=true"
read one email + its links
curl "https://api-testmail.codaipro.com/api/emails/EMAIL_ID"

# URLs extracted from the message
curl "https://api-testmail.codaipro.com/api/emails/EMAIL_ID/links"

# download raw .eml
curl -OJ "https://api-testmail.codaipro.com/api/emails/EMAIL_ID/download"
live stream (server-sent events)
curl -N "https://api-testmail.codaipro.com/api/emails/stream"
# event: connected
# data: {"message":"SSE connected","timestamp":...}

08 · Answers

FAQ & troubleshooting

Mail isn’t arriving

The two classic culprits: the namespace doesn’t match your testmail.app mailbox, or the free tier has hit its quota. Check /health for api_key_configured, then verify keys in the testmail.app console.

I still see demo emails

The backend only serves mock emails while TESTMAIL_API_KEY and TESTMAIL_NAMESPACE are blank. Add them and redeploy — no demo flag means no demo mail.

API calls fail in the browser

CORS. Add your hosting origin to the backend’s CORS_ORIGINS and re-deploy the backend.

My custom domain broke

The domain must be added in Vercel’s project settings and in your DNS provider. Vercel shows the exact record to create; update both sides together.

Is my API key safe?

Yes — the key lives only on the backend. The browser talks exclusively to your FastAPI gateway, which proxies testmail.app. Never ship the key in a frontend build.

Enough reading — print an address.

No account, no sign-up. Your address is ready the moment you open the home page.