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 file

Running 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; done

5. 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/client
import { 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.

Something wrong or missing? Edit this page on GitHub.