Quickstart
From nothing to an assertion on a real email. Five minutes, two paths — pick curl if you
are wiring up a language mailQA has no SDK for, and the SDK if you are writing tests in
JavaScript or TypeScript.
1. Get an API key
Sign in to the dashboard, open Settings → API keys, and create one. The plaintext key is displayed once, at creation — it is stored only as a SHA-256 hash, so a lost key is revoked and replaced, never recovered.
export MAILQA_API_KEY=mqa_live_9f2c1d… # a CI secret, never a committed fileRunning mailQA yourself? Point the SDK and these curl calls at your own deployment with
MAILQA_URL / baseUrl; the local development stack, its seeded key and its ports are
covered in TESTING.md.
Keys can be given an expiry (expiresInDays) and revoked at any time. Revoked keys are
kept, not deleted, so the record of which key was used when outlives the key.
2. Create an inbox
One throwaway inbox per test run is the pattern mailQA is built around: it makes every suite isolated by construction, and it means you never have to reason about a message left over from a previous run satisfying this run's wait.
curl -sX POST "$MAILQA_URL/v1/inboxes" \
-H "Authorization: Bearer $MAILQA_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"name":"ci-run-4417"}'{
"id": "clx7f0a1b0000qw3h2v9k1m4t",
"name": "ci-run-4417",
"address": "ci-run-4417@org-one.mailqa.io",
"kind": "MX",
"projectId": "clx6zz0000000qw3habcd123",
"messageCount": 0,
"isActive": true,
"createdById": null,
"createdAt": "2025-03-04T09:12:44.118Z",
"created": true,
"smtp": {
"host": "smtp.mailqa.io",
"ports": [587, 2525],
"username": "org-one",
"password": "…"
}
}smtp is your organization's SMTP sink credentials — one set, shared by every inbox —
attached so a CI job can configure its mailer in the same call. They are re-readable any
time from GET /v1/smtp/credentials: copying credentials into an app config twice is
normal, and forcing a rotation each time would be hostile.
3. Get mail into it
Either point the application under test at the SMTP credentials…
host smtp.mailqa.io (localhost when running locally)
port 587 or 2525
user <smtp.username>
pass <smtp.password>
…and have it send to inbox.address, which lands in this inbox exactly as it would over
MX — or let it send to inbox.address over the public internet, which arrives over inbound
MX. Both land in the same inbox and read identically.
Through the sink, mail is filed by the address it was sent to: send to anything else —
customer@example.com, say — and an inbox of that name appears in the list, created on
first use. Nothing is ever delivered, whichever address the app used.
Inbound MX stays closed until an organization owner has confirmed their email address; an unverified organization gets a
450(temporary, so well-behaved senders retry) and a banner in the dashboard. The SMTP sink is unaffected — it needs your credentials, so it is not an abuse vector.
On Pro and above you can receive on a domain of your own instead. Add one MX record
pointing test.yourcompany.com at mail.mailqa.io, press Check DNS in Settings →
Domain, and inbox.address starts reporting qa@test.yourcompany.com. It is the same
inbox — the your-org.mailqa.io address keeps working and moves to inbox.aliases — so
nothing you have already written needs changing. Use a subdomain that carries no real mail:
everything sent there is captured and never delivered onward.
4. Read the message
curl -s "$MAILQA_URL/v1/inboxes/$INBOX_ID/messages?subject_contains=Verify&limit=1" \
-H "Authorization: Bearer $MAILQA_API_KEY"That returns summaries. Fetch the full message — bodies, links, codes, headers, attachments — by id:
curl -s "$MAILQA_URL/v1/messages/$MESSAGE_ID" \
-H "Authorization: Bearer $MAILQA_API_KEY"The two fields most tests reach for are extracted for you:
{
"subject": "Verify your email address",
"codes": ["729104"],
"links": [{ "url": "https://acme.test/verify?token=tok_4417", "text": "Verify your email" }]
}A polling loop in shell looks like this — but if you are in JavaScript, don't write it; see the next section.
until curl -sf "$MAILQA_URL/v1/inboxes/$INBOX_ID/messages?subject_contains=Verify&limit=1" \
-H "Authorization: Bearer $MAILQA_API_KEY" | grep -q '"id"'; do sleep 1; done5. Tear down
curl -sX DELETE "$MAILQA_URL/v1/inboxes/$INBOX_ID" -H "Authorization: Bearer $MAILQA_API_KEY"DELETE /v1/inboxes/{id}/messages empties an inbox without deleting it, and returns the
number of messages removed — the teardown call for a suite that reuses one inbox across
tests.
The same thing with the SDK
npm install --save-dev @mailqa/clientimport { MailQA } from '@mailqa/client';
const mailqa = new MailQA({ apiKey: process.env.MAILQA_API_KEY! });
const inbox = await mailqa.inboxes.create({ name: `ci-${process.env.GITHUB_RUN_ID}` });
await registerUser(inbox.address); // your app under test
const message = await mailqa.waitForMessage({
inbox: inbox.id,
subjectContains: 'Verify your email',
timeout: 30_000,
});
expect(message.codes[0]).toMatch(/^\d{6}$/);
await page.goto(message.links[0].url);
await mailqa.inboxes.delete(inbox.id);waitForMessage polls until something matches or the timeout expires, then returns the
full message. It exists so a test never contains await sleep(5000) — the line that makes
suites both slow and flaky. On timeout it throws a MessageTimeoutError carrying the
filter you used, because "the email never arrived" is the failure you will actually have to
debug.
Next: the full REST API reference or the full SDK reference.
