TestTau docs
TestTau has two modes: public test tools for fast checks, and signed-in private tools when captures need account ownership and API-key protection.
Public vs private
| Public mail | Send to <name>@mail.testtau.com. Anyone who knows the inbox name can open it. Use it for anonymous QA, smoke tests, and public signup checks. |
| Private mail | Sign in, choose one private inbox address, and copy its API key. Mail sent to that address requires Authorization: Bearer <key> to read. |
| Public hooks | Send requests to https://hook.testtau.com/<hookId>. Capture is public, but inspection, config, replay, wipe, and assertion APIs require the hook admin token claimed by the inspector. |
| Private hooks | Sign in and create one private hook. Webhook senders can post to the capture URL, but inspection, replay, config, wipe, and API reads require the hook key. |
First success flows
| Wait for email JSON | Create an inbox name, send an email to <name>@mail.testtau.com, then call /api/wait?timeout=15000. |
| Assert webhook capture | Claim a hook admin token, record START=$(date +%s%3N), trigger your app, then call /api/assert?since=$START&min_count=1 with Authorization: Bearer <token>. |
| Read private mail | Create private mail in Account, paste the key in the private inbox UI, or use Authorization: Bearer $TESTTAU_MAIL_KEY. |
| Inspect private hooks | Create private hooks in Account, send to the capture URL, then inspect with Authorization: Bearer $TESTTAU_HOOK_KEY. |
Use public mail when the inbox can be shared by name. Use private mail when an account-owned test inbox should require a bearer token to read.
Quick start
checkout-qa@mail.testtau.com
https://mail.testtau.com/i/checkout-qa
Wait from automation
The wait endpoint long-polls for up to 15 seconds and returns the newest matching message.
curl "https://mail.testtau.com/i/checkout-qa/api/wait?timeout=15000&subject=Verify&text=884201"
Supported filters are from, to, subject, preview, text, hasAttachments, and after (epoch milliseconds).
Signed-in private mail
Free accounts can create one private inbox address and a mail API key. When TestTau receives mail to that private address, it stores the message separately from anonymous public inboxes.
| Public inbox | Anyone with the inbox URL can read and delete messages. Best for random QA and smoke tests. |
| Private inbox | Mail sent to your private address requires Authorization: Bearer <key> to read. |
The account page includes a private mail wait helper. Paste the private mail key, click Wait for test email, then send a message to the private address. The helper waits 15 seconds and confirms whether a new private message arrived.
# Recipient: your account-owned private address
login-smoke@mail.testtau.com
# Browser viewer
https://mail.testtau.com/private/i/login-smoke
# Paste the key in the browser. TestTau stores it in tab session storage
# and sends it as an Authorization header, not in the URL.
# API list
curl -H "Authorization: Bearer $TESTTAU_MAIL_KEY" \
"https://mail.testtau.com/private/i/login-smoke/api/list"
# Wait for a matching private message
curl -H "Authorization: Bearer $TESTTAU_MAIL_KEY" \
"https://mail.testtau.com/private/i/login-smoke/api/wait?timeout=15000&subject=Verify"
# Parsed JSON for one message
curl -H "Authorization: Bearer $TESTTAU_MAIL_KEY" \
"https://mail.testtau.com/private/i/login-smoke/api/message/<messageId>/json"
Mail API
curl "https://mail.testtau.com/i/checkout-qa/api/quota"
curl "https://mail.testtau.com/i/checkout-qa/api/list"
curl "https://mail.testtau.com/i/checkout-qa/api/message/<messageId>"
curl "https://mail.testtau.com/i/checkout-qa/api/message/<messageId>/json"
curl -X DELETE "https://mail.testtau.com/i/checkout-qa/api/message/<messageId>"
curl -X DELETE "https://mail.testtau.com/i/checkout-qa/api/all"
The message JSON endpoint returns the parsed email content: sender, recipient, subject, preview, plain text, HTML, attachment metadata, and download links for the raw .eml plus each attachment.
{
"id": "01...",
"fromAddr": "sender@example.com",
"toAddr": "checkout-qa@mail.testtau.com",
"subject": "Verify your email",
"textBody": "Your code is 884201",
"htmlBody": "<p>Your code is <strong>884201</strong></p>",
"attachments": [
{
"id": "01...",
"filename": "receipt.pdf",
"contentType": "application/pdf",
"sizeBytes": 12000,
"downloadUrl": "/i/checkout-qa/api/message/01.../attachment/01..."
}
],
"links": {
"raw": "/i/checkout-qa/api/message/01.../raw"
}
}
Mail limits
| Message size | 25 MiB |
| Attachment size | 5 MiB |
| Attachments per message | 10 |
| Stored messages per inbox | 100 |
| Signed-in private mail | 1 private inbox address per account |
| HTTP API rate limit | 120 requests/minute per IP |
| Inbound sender limit | 100 messages/hour per sender address |
| Inbound domain limit | 500 messages/hour per sender domain |
| Retention | 48 h |
Hooks
Use public hooks for quick webhook inspection and CI gates. Use private hooks when captured requests should require an account API key to inspect, replay, configure, or wipe.
Quick start
HOOK_ID="my-hook-id"
HOOK_ADMIN_TOKEN="$(curl -fsS -X POST "https://hook.testtau.com/_/$HOOK_ID/api/admin-token" | jq -r .token)"
curl -X POST "https://hook.testtau.com/$HOOK_ID" \
-H 'content-type: application/json' \
-d '{"event":"hello"}'
curl -H "Authorization: Bearer $HOOK_ADMIN_TOKEN" \
"https://hook.testtau.com/_/$HOOK_ID/api/list"
The browser inspector claims the same admin token automatically on first load and stores it in tab session storage. Copy it before opening the hook elsewhere.
Signed-in private hooks
Free accounts can create one private hook. External services can send to the hook URL without a key, but reading captures, changing config, replaying, wiping, and assertion APIs require Authorization: Bearer <key>.
# Send a capture
curl -X POST https://hook.testtau.com/private/<hookId> \
-H 'content-type: application/json' \
-d '{"event":"private"}'
# Browser inspector
https://hook.testtau.com/private/_/<hookId>
# Paste the key in the browser. TestTau stores it in tab session storage
# and sends it as an Authorization header, not in the URL.
# Read captures
curl -H "Authorization: Bearer $TESTTAU_HOOK_KEY" \
"https://hook.testtau.com/private/_/<hookId>/api/list"
# Get one captured request
curl -H "Authorization: Bearer $TESTTAU_HOOK_KEY" \
"https://hook.testtau.com/private/_/<hookId>/api/request/<requestId>"
# Assert recent captures from CI
curl -fsS -H "Authorization: Bearer $TESTTAU_HOOK_KEY" \
"https://hook.testtau.com/private/_/<hookId>/api/assert?since=$START&min_count=1"
What is captured
- Method, full path, query string, and headers.
- Client IP and country from Cloudflare.
- Full request body up to 1 MiB.
- Schema assertion status when configured.
Config, replay, and assertions
Open the inspector to change the response status/body, replay a captured request against another URL, or attach a JSON Schema. Schema failures can either be recorded in the inspector or rejected immediately with 422.
# Configure the hook response
curl -X PUT "https://hook.testtau.com/_/<hookId>/api/config" \
-H "Authorization: Bearer $HOOK_ADMIN_TOKEN" \
-H 'content-type: application/json' \
-d '{"responseStatus":202,"responseContentType":"application/json","responseBody":"{"accepted":true}"}'
# Private hook config uses the same API plus the bearer token
curl -X PUT "https://hook.testtau.com/private/_/<hookId>/api/config" \
-H "Authorization: Bearer $TESTTAU_HOOK_KEY" \
-H 'content-type: application/json' \
-d '{"responseStatus":202,"responseContentType":"application/json","responseBody":"{"accepted":true}"}'
{
"type": "object",
"required": ["event", "user_id"],
"properties": {
"event": { "type": "string" },
"user_id": { "type": "string" }
},
"additionalProperties": false
}
CI gating
START=$(date +%s%3N)
curl -fsS \
-H "Authorization: Bearer $HOOK_ADMIN_TOKEN" \
"https://hook.testtau.com/_/<hookId>/api/assert?since=$START&min_count=1"
The assert endpoint returns 200 when all matching captures passed and 422 when captures are missing or failed validation.
Replay
# Replay a public capture
curl -X POST \
-H "Authorization: Bearer $HOOK_ADMIN_TOKEN" \
"https://hook.testtau.com/_/<hookId>/api/replay/<requestId>?target=https%3A%2F%2Fexample.com%2Fwebhook"
# Replay a private capture
curl -X POST \
-H "Authorization: Bearer $TESTTAU_HOOK_KEY" \
"https://hook.testtau.com/private/_/<hookId>/api/replay/<requestId>?target=https%3A%2F%2Fexample.com%2Fwebhook"
Hook limits
| Body size | 1 MiB |
| Retention | 48 h |
| Capture rate limit | 120 requests/minute per IP |
| Hook id rate limit | 300 requests/minute per hook id |
| Signed-in private hooks | 1 private hook per account |
| Schema size | ~50 KiB |
| Errors returned per request | first 20 |
Recipes
Use these as copy-paste starting points for the workflows people usually try first.
Playwright: wait for a signup email
import { expect, test } from '@playwright/test';
test('signup email arrives', async ({ page }) => {
const inbox = `qa-${crypto.randomUUID().slice(0, 8)}`;
await page.goto('https://your-app.example/signup');
await page.fill('input[type=email]', `${inbox}@mail.testtau.com`);
await page.click('button[type=submit]');
const message = await fetch(
`https://mail.testtau.com/i/${inbox}/api/wait?timeout=15000&subject=Verify`,
).then((res) => res.json());
expect(message.ok).toBe(true);
});
CI: assert a webhook was received
HOOK="ci-checkout-${GITHUB_RUN_ID:-local}"
HOOK_ADMIN_TOKEN="$(curl -fsS -X POST "https://hook.testtau.com/_/$HOOK/api/admin-token" | jq -r .token)"
START=$(date +%s%3N)
# Trigger the system under test here.
curl -fsS \
-H "Authorization: Bearer $HOOK_ADMIN_TOKEN" \
"https://hook.testtau.com/_/$HOOK/api/assert?since=$START&min_count=1"
Agent: give a coding agent the skill
Use TestTau for test mail and webhook tests.
Skill URL: https://testtau.com/skills/testtau/SKILL.md
API docs: https://testtau.com/skills/testtau/references/api.md
Index: https://testtau.com/llms.txt
Private mail: wait with a bearer token
export TESTTAU_MAIL_KEY="ttk_free_..."
INBOX="your-private-inbox"
curl -fsS -H "Authorization: Bearer $TESTTAU_MAIL_KEY" \
"https://mail.testtau.com/private/i/$INBOX/api/wait?timeout=15000&subject=Verify"
Private hooks: assert a keyed capture
export TESTTAU_HOOK_KEY="ttk_hook_..."
HOOK_ID="your-private-hook"
START=$(date +%s%3N)
# Trigger your app or provider webhook here.
curl -fsS -H "Authorization: Bearer $TESTTAU_HOOK_KEY" \
"https://hook.testtau.com/private/_/$HOOK_ID/api/assert?since=$START&min_count=1"
Agent Skills
TestTau ships an agent skill so coding agents can create public test inboxes, wait for email, fetch parsed message JSON, capture webhooks, and run schema assertions without learning the API from scratch.
Agent quickstart
- Use public mail or hooks for anonymous tests that can be read by name.
- Use private mail or private hooks when the capture must require
Authorization: Bearer <key>. - Install or reference the hosted skill, then point the agent at the API reference for exact endpoints and limits.
Discovery URLs
| Agent discovery | https://testtau.com/llms.txt |
| Skill | https://testtau.com/skills/testtau/SKILL.md |
| API reference | https://testtau.com/skills/testtau/references/api.md |
| ClawHub/OpenClaw | testtau@1.0.0 |
| Hugging Face | testtau/testtau-agent-skill |
Public mail from an agent
INBOX="agent-login-$(openssl rand -hex 4)"
echo "Send mail to $INBOX@mail.testtau.com"
curl -fsS "https://mail.testtau.com/i/$INBOX/api/wait?timeout=15000"
Public webhook from an agent
HOOK="agent-hook-$(openssl rand -hex 4)"
HOOK_ADMIN_TOKEN="$(curl -fsS -X POST "https://hook.testtau.com/_/$HOOK/api/admin-token" | jq -r .token)"
curl -fsS -X POST "https://hook.testtau.com/$HOOK" \
-H "content-type: application/json" \
-d '{"event":"agent.test"}'
curl -fsS -H "Authorization: Bearer $HOOK_ADMIN_TOKEN" \
"https://hook.testtau.com/_/$HOOK/api/list"
Private captures
Private mail and private hooks use account-owned keys. Agents should send keys with Authorization: Bearer <key> and should never place TestTau API keys in URLs.
curl -H "Authorization: Bearer $TESTTAU_MAIL_KEY" \
"https://mail.testtau.com/private/i/<inbox>/api/wait?timeout=15000"
curl -H "Authorization: Bearer $TESTTAU_HOOK_KEY" \
"https://hook.testtau.com/private/_/<hookId>/api/list"