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.
01 · Workflow
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.
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
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.
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
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
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
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.
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.
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
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.
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.
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
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.
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
Everything is plain environment variables — no config files to drift.
| Variable | Description | Default |
|---|---|---|
| TESTMAIL_API_KEY | Your testmail.app API key (Secrets → API keys → copy) | "" → demo mode |
| TESTMAIL_NAMESPACE | Your testmail.app mailbox / namespace name | "" → demo mode |
| API_KEY | Legacy alias for TESTMAIL_API_KEY | — |
| NAMESPACE | Legacy alias for TESTMAIL_NAMESPACE | — |
| BASE_URL | testmail.app API base | https://api.testmail.app/api |
| CORS_ORIGINS | Comma-separated origins allowed to call the API | localhost:5173 + codaipro.com |
| DEBUG | Enable debug mode (required for ALLOW_DEMO_MODE) | false |
| ALLOW_DEMO_MODE | Serve mock emails when no credentials are set | false |
| EMAIL_RETENTION_MINUTES | Auto-hide retention window in minutes (older emails purged) | 10 |
| ADMIN_PASSWORD | Admin access key: console login, archive mode, telemetry feed (no default) | required in prod |
| SELF_PING_URL | Your service URL (keep-alive ping target, idle-shutdown hosts only) | null |
| SELF_PING_ENABLED | Ping yourself to avoid free-tier spin-down | false |
| PING_INTERVAL_MINUTES | Minutes between keep-alive pings | 10 |
| TRUSTED_PROXY_NETWORKS | CIDRs whose forwarded client IP may be trusted (empty = trust nobody) | |
| ATTACHMENT_MAX_BYTES | Cap for a proxied attachment or .eml download | 10000000 |
| TESTER_ALLOWED_RECIPIENT_DOMAINS | Domains the anonymous email tester may deliver to | inbox.testmail.app |
| Variable | Description | Default |
|---|---|---|
| VITE_API_URL | Backend base URL your build talks to | https://api-testmail.codaipro.com |
In CI these come from repository secrets with sensible fallbacks — see the workflow file for the exact wiring.
07 · Wire
Every endpoint the UI uses, directly scriptable. Swap the host for your own backend URL.
| POST | /api/emails/generate | Generate 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}/links | Extract unique URLs from the email |
| GET | /api/emails/{id}/download | Download the raw email as RFC 822 .eml |
| GET | /api/emails/attachments/{id}/{filename} | Securely proxy an attachment download |
| GET | /api/emails/stream | Server-Sent Events stream (live inbox) |
| GET | /api/tags | Unique tags with counts (alias: /api/emails/tags) |
| GET | /health, /ping | Health + 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-Key | Admin key: console login, unlimited archive, telemetry, private mailboxes |
| X-Mailbox-Key | Recovery 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 |
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"}# 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×tamp_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"
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"
curl -N "https://api-testmail.codaipro.com/api/emails/stream"
# event: connected
# data: {"message":"SSE connected","timestamp":...}08 · Answers
/health for api_key_configured, then verify keys in the testmail.app console.TESTMAIL_API_KEY and TESTMAIL_NAMESPACE are blank. Add them and redeploy — no demo flag means no demo mail.CORS_ORIGINS and re-deploy the backend.No account, no sign-up. Your address is ready the moment you open the home page.