Signup verification
Create inbox → enter its address in the signup form → submit → WAIT by subject → extract the verification link → check its host/path and open it in your test.
Receive and inspect real emails in automated tests.
For Playwright, Cypress, Selenium, Postman, backend tests and CI/CD.
Loading…
Temporary inboxes · Custom prefix · TTL · Wait · Subject, sender and recipient filters · Text / HTML · Attachments · Raw MIME · Ordered headers · OTP candidates · Links
Monthly counters reset at 00:00 UTC on the first day. Rejected deliveries do not count. Reads and downloads do not use monthly quota. Deletion and expiry do not refund quota.
Send API requests to QAP Tools. Your application under test only sends email to the temporary address returned by the API.
https://qaptools.com/api/email/v1Email API receives email. It does not send email from your application.
For these examples, select inboxes:create, messages:wait and messages:read. Add extractions:read for OTP/link checks and attachments:read or raw:read for downloads.
These examples create an inbox and pause so you can trigger an email in your application. They expect a subject containing Welcome; adapt the assertions to your email. cURL uses Bash, jq and uuidgen; JavaScript uses Node.js 20+; Python uses the standard library.
set -eu
BASE="https://qaptools.com/api/email/v1"
# QAP_EMAIL_API_TOKEN comes from your environment or CI secrets.
IDEMPOTENCY_KEY=$(uuidgen) # Keep this key when retrying this request.
BOX=$(curl --fail-with-body -sS "$BASE/inboxes" \
-H "Authorization: Bearer $QAP_EMAIL_API_TOKEN" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{"prefix":"signup","ttl":"2h"}')
INBOX_ID=$(printf '%s' "$BOX" | jq -r .id)
printf '%s' "$BOX" | jq -r .address
read -r -p "Send a Welcome email from your app to this address, then press Enter: " _
RESULT=$(curl --fail-with-body -sS \
"$BASE/inboxes/$INBOX_ID/wait?timeout=20&subject_contains=Welcome" \
-H "Authorization: Bearer $QAP_EMAIL_API_TOKEN")
printf '%s' "$RESULT" | jq -e \
'.status == "matched" and (.message.subject | contains("Welcome"))'All HTTP API operations use a scoped Bearer token. Your dashboard session alone does not authorize API requests. Documentation is public; inboxes require an account.
Sign in on this page, then open Email API dashboard → API Tokens → Create API token. Choose only the scopes your test needs.
Authorization: Bearer $QAP_EMAIL_API_TOKEN| Scopes | Purpose |
|---|---|
inboxes:create | Create temporary inboxes |
inboxes:read | Read inbox metadata |
inboxes:delete | Delete an inbox and revoke access to its content |
messages:read | Read accepted messages, headers and attachment metadata |
messages:wait | Wait for a matching accepted message |
raw:read | Download raw MIME |
attachments:read | Download attachments |
extractions:read | Read OTP and link candidates |
usage:read | Read Email API usage and remaining quota |
Create inbox → enter its address in the signup form → submit → WAIT by subject → extract the verification link → check its host/path and open it in your test.
Create inbox → trigger OTP → WAIT for the message → read OTP candidates → assert or use the expected code.
Create inbox → request a password reset for that address → WAIT by subject → extract and check the reset link.
request calls QAP Tools; page visits your application under test. Replace the application URL, form labels and expected subject with your own. This example needs inboxes:create and messages:wait. Allow enough test time for WAIT.
import { test, expect } from "@playwright/test";
import { randomUUID } from "node:crypto";
test("signup email", async ({ page, request }) => {
test.setTimeout(60_000);
const base = "https://qaptools.com/api/email/v1";
const headers = { Authorization: "Bearer " + process.env.QAP_EMAIL_API_TOKEN };
const created = await request.post(base + "/inboxes", {
headers: { ...headers, "Idempotency-Key": randomUUID() },
data: { prefix: "signup", ttl: "2h" },
});
expect(created.ok()).toBeTruthy();
const inbox = await created.json();
// Your application: replace the URL and form selectors.
await page.goto("https://app-under-test.example/signup");
await page.getByLabel("Email").fill(inbox.address);
await page.getByRole("button", { name: "Sign up" }).click();
// QAP Tools: wait for the email sent by your application.
const waited = await request.get(base + "/inboxes/" + inbox.id +
"/wait?timeout=20&subject_contains=Welcome", { headers, timeout: 35_000 });
expect(waited.ok()).toBeTruthy();
const result = await waited.json();
expect(result.status).toBe("matched");
expect(result.message.subject).toContain("Welcome");
});Add this after a matched WAIT in the Playwright example. It needs messages:read and extractions:read. Replace the expected OTP 123456 and welcome.pdf with values from your test fixture. A candidate is not a guaranteed OTP; check the expected link host and path before using it.
// Inside the same test, after a matched WAIT.
const path = base + "/inboxes/" + inbox.id + "/messages/" + result.message.id;
const detail = await request.get(path, { headers });
expect(detail.ok()).toBeTruthy();
const message = await detail.json();
expect(message.text).toContain("Welcome");
expect(message.attachments.map(item => item.filename)).toContain("welcome.pdf");
const extracted = await request.get(path + "/extractions", { headers });
expect(extracted.ok()).toBeTruthy();
const candidates = await extracted.json();
expect(candidates.otp_candidates.map(item => item.value)).toContain("123456");
const verification = candidates.links.find(item => {
const url = new URL(item.url);
return url.origin === "https://app-under-test.example" && url.pathname === "/verify";
});
expect(verification).toBeDefined();
await page.goto(verification.url);Also works with Cypress · Selenium · Postman · backend tests · CI/CD.
Paths are relative to the QAP Tools base URL. Every request needs Authorization. Replace {id}, {messageId} and {attachmentId} with returned UUIDs; set INBOX_ID, MESSAGE_ID and ATTACHMENT_ID to those values for cURL. Before a new creation, set IDEMPOTENCY_KEY=$(uuidgen); keep it when retrying. Only the documented query parameters are supported. Common authentication, rate and service errors also apply to every endpoint.
Responses below use illustrative values. Always use the IDs, address and download URL returned by the API; mail.example.invalid is not a receiving domain.
POST /inboxesCreate a temporary inboxRequired scope: inboxes:create
Parameters: Headers: Authorization, Idempotency-Key (1–200 printable ASCII characters, nonblank), Content-Type: application/json. Body: optional prefix (default test), optional ttl (default 2h); max 1024 bytes. TTL: 15m, 30m, 1h, 2h, 4h. Prefix: 1–31 ASCII letters/digits/hyphens, trimmed and lowercased, no leading/trailing hyphen. Reserved: admin, postmaster, abuse, support, root, system, qap, noreply, no-reply. Same key + same normalized input replays the original creation snapshot, including status active; GET gives the current status. Reuse the key on retries, use a new key for a new inbox.
Request example
curl --globoff --fail-with-body -sS -X POST "https://qaptools.com/api/email/v1/inboxes" \
-H "Authorization: Bearer $QAP_EMAIL_API_TOKEN" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" \
-d '{"prefix":"signup","ttl":"2h"}'Successful response · 201 / 200 (replay)
{
"id": "11111111-1111-4111-8111-111111111111",
"address": "signup-0123456789abcdef0123456789abcdef@mail.example.invalid",
"created_at": "2026-10-05T10:00:00.000Z",
"expires_at": "2026-10-05T12:00:00.000Z",
"status": "active"
}Key errors: IDEMPOTENCY_KEY_REQUIRED · INVALID_PREFIX · INVALID_TTL · IDEMPOTENCY_CONFLICT · ACTIVE_INBOX_LIMIT · INBOX_MONTHLY_QUOTA_EXCEEDED
GET /inboxesList active inboxesRequired scope: inboxes:read
Parameters: No body or query. Active inboxes only, maximum 5, ordered by creation time then ID. No history listing.
Request example
curl --globoff --fail-with-body -sS -X GET "https://qaptools.com/api/email/v1/inboxes" \
-H "Authorization: Bearer $QAP_EMAIL_API_TOKEN"Successful response · 200
{
"inboxes": [
{
"id": "11111111-1111-4111-8111-111111111111",
"address": "signup-0123456789abcdef0123456789abcdef@mail.example.invalid",
"created_at": "2026-10-05T10:00:00.000Z",
"expires_at": "2026-10-05T12:00:00.000Z",
"status": "active"
}
]
}Key errors: INVALID_REQUEST
GET /inboxes/{id}Read current inbox metadataRequired scope: inboxes:read
Parameters: id: inbox UUID. No body or query. status is active, expired or deleted; retained metadata is readable after expiry/deletion.
Request example
curl --globoff --fail-with-body -sS -X GET "https://qaptools.com/api/email/v1/inboxes/$INBOX_ID" \
-H "Authorization: Bearer $QAP_EMAIL_API_TOKEN"Successful response · 200
{
"id": "11111111-1111-4111-8111-111111111111",
"address": "signup-0123456789abcdef0123456789abcdef@mail.example.invalid",
"created_at": "2026-10-05T10:00:00.000Z",
"expires_at": "2026-10-05T12:00:00.000Z",
"status": "active"
}Key errors: INBOX_NOT_FOUND
DELETE /inboxes/{id}Delete an inboxRequired scope: inboxes:delete
Parameters: id: inbox UUID. No body or query. Repeating deletion for your inbox is safe. Content access ends; monthly quota is not refunded.
Request example
curl --globoff --fail-with-body -sS -X DELETE "https://qaptools.com/api/email/v1/inboxes/$INBOX_ID" \
-H "Authorization: Bearer $QAP_EMAIL_API_TOKEN"Successful response · 204
204 No Content — empty response body.
Key errors: INBOX_NOT_FOUND
Filters combine with AND. subject_contains is a case-insensitive literal substring (1–512 UTF-8 bytes, no control characters). sender and recipient match full envelope addresses: domain case is normalized; local-part case is preserved. Use the complete returned inbox address for recipient. Unknown or repeated parameters are rejected.
GET /inboxes/{id}/messagesList accepted messagesRequired scope: messages:read
Parameters: id: inbox UUID. Optional subject_contains, sender, recipient; combined with AND. Accepted, available messages only, in stable order. Hard maximum 20 accepted emails per inbox lifetime; no pagination or cursor. Filters do not change quota accounting.
Request example
curl --globoff --fail-with-body -sS -X GET "https://qaptools.com/api/email/v1/inboxes/$INBOX_ID/messages?subject_contains=Welcome" \
-H "Authorization: Bearer $QAP_EMAIL_API_TOKEN"Successful response · 200
{
"messages": [
{
"id": "22222222-2222-4222-8222-222222222222",
"from": "hello@app.example.invalid",
"to": [
"signup-0123456789abcdef0123456789abcdef@mail.example.invalid"
],
"subject": "Welcome",
"received_at": "2026-10-05T10:01:00.000Z",
"has_attachments": true
}
],
"admission_pending": false
}Key errors: INVALID_FILTER · INBOX_NOT_FOUND · INBOX_EXPIRED · INBOX_DELETED · ADMISSION_PENDING
GET /inboxes/{id}/messages/{messageId}Read a message and attachment metadataRequired scope: messages:read
Parameters: id, messageId: UUIDs. No body or query. from/to are envelope addresses; to contains this inbox address. subject, text, html and content_type can be null. size is bytes. HTML is untrusted data. Full headers are separate. Attachment downloads require attachments:read.
Request example
curl --globoff --fail-with-body -sS -X GET "https://qaptools.com/api/email/v1/inboxes/$INBOX_ID/messages/$MESSAGE_ID" \
-H "Authorization: Bearer $QAP_EMAIL_API_TOKEN"Successful response · 200
{
"id": "22222222-2222-4222-8222-222222222222",
"from": "hello@app.example.invalid",
"to": [
"signup-0123456789abcdef0123456789abcdef@mail.example.invalid"
],
"subject": "Welcome",
"received_at": "2026-10-05T10:01:00.000Z",
"text": "Welcome! Your code is 123456.",
"html": "<p>Welcome! Your code is 123456.</p>",
"attachments": [
{
"id": "33333333-3333-4333-8333-333333333333",
"filename": "welcome.pdf",
"content_type": "application/pdf",
"size": 1024
}
]
}Key errors: MESSAGE_NOT_FOUND · INBOX_EXPIRED · INBOX_DELETED · ADMISSION_PENDING
WAIT is a long-poll for the first available matching accepted message. Default timeout: 20 seconds; allowed: 1–25 seconds. Maximum: 2 concurrent waits per account, shared across tokens and inboxes. Internal polling does not consume extra external API rate slots.
Pass the returned after value to your next WAIT call to avoid receiving the same message again. Keep it as a decimal string. Without after (default 0), the same message may be returned. Timeout does not advance it. Changing filters keeps earlier positions excluded; use after=0 to search from the beginning.
WAIT returns message metadata. Read /messages/{messageId} for body and attachment metadata; full headers have a separate endpoint.
Next WAIT request
GET /inboxes/{id}/wait?timeout=20&subject_contains=Welcome&after=123Timeout is a normal HTTP 200 response
{
"status": "timeout",
"message": null
}GET /inboxes/{id}/waitWait for a matching accepted messageRequired scope: messages:wait
Parameters: id: inbox UUID. Optional timeout: integer 1–25, default 20; after: decimal string 0–9223372036854775807, default 0. The same subject_contains, sender and recipient filters apply. messages:read is not needed for WAIT itself.
Request example
curl --globoff --fail-with-body -sS -X GET "https://qaptools.com/api/email/v1/inboxes/$INBOX_ID/wait?timeout=20&subject_contains=Welcome" \
-H "Authorization: Bearer $QAP_EMAIL_API_TOKEN"Successful response · 200
{
"status": "matched",
"message": {
"id": "22222222-2222-4222-8222-222222222222",
"from": "hello@app.example.invalid",
"to": [
"signup-0123456789abcdef0123456789abcdef@mail.example.invalid"
],
"subject": "Welcome",
"received_at": "2026-10-05T10:01:00.000Z",
"has_attachments": true
},
"after": "123"
}Key errors: INVALID_WAIT_TIMEOUT · INVALID_FILTER · INVALID_REQUEST · CONCURRENT_WAIT_LIMIT · INBOX_NOT_FOUND · INBOX_EXPIRED · INBOX_DELETED · ADMISSION_PENDING
GET /usageRead current usage and remaining quotaRequired scope: usage:read
Parameters: No body or query. Current calendar month UTC (YYYY-MM), limits, used and remaining monthly quota; used.active_inboxes is the current active count. Counters reset at 00:00:00 UTC on the first day. Accepted emails count in the month they were received. Deletion/expiry do not refund quota.
Request example
curl --globoff --fail-with-body -sS -X GET "https://qaptools.com/api/email/v1/usage" \
-H "Authorization: Bearer $QAP_EMAIL_API_TOKEN"Successful response · 200
{
"month": "2026-10",
"limits": {
"inboxes_created": 200,
"accepted_deliveries": 300,
"active_inboxes": 5,
"messages_per_inbox": 20
},
"used": {
"inboxes_created": 1,
"accepted_deliveries": 1,
"active_inboxes": 1
},
"remaining": {
"inboxes_created": 199,
"accepted_deliveries": 299
}
}Key errors: ADMISSION_PENDING · BACKEND_UNAVAILABLE
Raw MIME and attachment endpoints return a short-lived download URL. Its configured lifetime is at most 30 seconds and can be shorter. Once issued, it may remain usable briefly after inbox deletion or token revocation. Fetch the returned URL directly; do not send your QAP API token to it.
GET /inboxes/{id}/messages/{messageId}/rawGet a raw MIME download URLRequired scope: raw:read
Parameters: id, messageId: UUIDs from inbox/message responses. No body or query. Download the .eml file using the returned URL.
Request example
curl --globoff --fail-with-body -sS -X GET "https://qaptools.com/api/email/v1/inboxes/$INBOX_ID/messages/$MESSAGE_ID/raw" \
-H "Authorization: Bearer $QAP_EMAIL_API_TOKEN"Successful response · 200
{
"url": "https://download.example.invalid/signed-example",
"expires_at": "2026-10-05T10:02:30.000Z"
}Key errors: INBOX_NOT_FOUND · MESSAGE_NOT_FOUND · INBOX_EXPIRED · INBOX_DELETED · STORAGE_UNAVAILABLE
GET /inboxes/{id}/messages/{messageId}/attachments/{attachmentId}Get an attachment download URLRequired scope: attachments:read
Parameters: id, messageId, attachmentId: UUIDs. Get attachmentId from the message detail. No body or query. size is bytes; content_type may be null. An inaccessible message returns MESSAGE_NOT_FOUND; an accessible message with a missing/wrong attachment returns ATTACHMENT_NOT_FOUND.
Request example
curl --globoff --fail-with-body -sS -X GET "https://qaptools.com/api/email/v1/inboxes/$INBOX_ID/messages/$MESSAGE_ID/attachments/$ATTACHMENT_ID" \
-H "Authorization: Bearer $QAP_EMAIL_API_TOKEN"Successful response · 200
{
"id": "33333333-3333-4333-8333-333333333333",
"filename": "welcome.pdf",
"content_type": "application/pdf",
"size": 1024,
"url": "https://download.example.invalid/signed-example",
"expires_at": "2026-10-05T10:02:30.000Z"
}Key errors: INBOX_NOT_FOUND · MESSAGE_NOT_FOUND · ATTACHMENT_NOT_FOUND · INBOX_EXPIRED · INBOX_DELETED · STORAGE_UNAVAILABLE
GET /inboxes/{id}/messages/{messageId}/headersRead structured MIME headersRequired scope: messages:read
Parameters: id, messageId: UUIDs. No body or query. Order, original name casing and repeated headers are preserved; folded values are unfolded. RFC 2047 encoded words are not decoded. Header section limit: 128 KiB.
Request example
curl --globoff --fail-with-body -sS -X GET "https://qaptools.com/api/email/v1/inboxes/$INBOX_ID/messages/$MESSAGE_ID/headers" \
-H "Authorization: Bearer $QAP_EMAIL_API_TOKEN"Successful response · 200
{
"headers": [
{
"name": "Subject",
"value": "Welcome"
},
{
"name": "X-Test",
"value": "first"
},
{
"name": "X-Test",
"value": "second"
}
]
}Key errors: INBOX_NOT_FOUND · MESSAGE_NOT_FOUND · INBOX_EXPIRED · INBOX_DELETED · HEADERS_TOO_LARGE · INVALID_MIME_HEADERS · STORAGE_UNAVAILABLE
GET /inboxes/{id}/messages/{messageId}/extractionsRead OTP candidates and linksRequired scope: extractions:read
Parameters: id, messageId: UUIDs. No body or query. Deterministic standalone numeric candidates of 4–8 digits (max 20), not guaranteed OTPs. Absolute HTTP/HTTPS links from text and HTML (max 100); URLs are never fetched. source is text or html. Duplicates keep their first source.
Request example
curl --globoff --fail-with-body -sS -X GET "https://qaptools.com/api/email/v1/inboxes/$INBOX_ID/messages/$MESSAGE_ID/extractions" \
-H "Authorization: Bearer $QAP_EMAIL_API_TOKEN"Successful response · 200
{
"otp_candidates": [
{
"value": "123456",
"source": "text"
}
],
"links": [
{
"url": "https://app.example.invalid/verify?code=example",
"source": "html"
}
]
}Key errors: INBOX_NOT_FOUND · MESSAGE_NOT_FOUND · INBOX_EXPIRED · INBOX_DELETED · BACKEND_UNAVAILABLE
Errors return a stable code, a safe message and a request_id. Keep the request ID when reporting a failure.
{
"error": {
"code": "ACTIVE_INBOX_LIMIT",
"message": "The active inbox limit has been reached.",
"request_id": "44444444-4444-4444-8444-444444444444"
}
}| HTTP | Errors / What to do |
|---|---|
| 400 |
Check the body, headers and supported parameters. |
| 401 |
Supply a valid, active Bearer token. |
| 403 |
Use a token with the endpoint's required scope. |
| 404 |
Check IDs and ownership. Message content may be unavailable or expired. |
| 409 |
Retry the original input, or use a new key for a new inbox. |
| 410 |
Create a new inbox; this inbox's content is no longer accessible. |
| 413 |
The MIME header section exceeds the supported size. |
| 422 |
The stored MIME headers cannot be parsed. |
| 429 |
Inspect Retry-After. Reduce requests/waits, free an active slot or wait for the monthly reset, as applicable. |
| 503 |
Retry with backoff. Reuse the same key and input if retrying inbox creation. |
Rate: 60 requests per rolling 60 seconds per account, shared by all tokens. One WAIT uses one external request slot. For HTTP 429, inspect Retry-After (seconds). For quota and active-inbox limits this is a backoff hint, not a promise that capacity will be available then.
ADMISSION_PENDING means received emails are still being prepared for reading. Retry with backoff; it is not a normal WAIT timeout or a partial successful response.