---
title: "Node.js client | Proxy Mail QA"
description: "proxymail-qa on npm: a typed Node.js client to create disposable test inboxes and wait for codes and links from Playwright, Cypress, Vitest or Jest."
url: https://proxymail.qa/docs/node
---

# Node.js client

[proxymail-qa](https://www.npmjs.com/package/proxymail-qa) wraps the [REST API](https://proxymail.qa/docs/rest-api) for JavaScript and TypeScript: Playwright, Cypress tasks, Vitest, Jest, Detox or a plain script. It has no dependencies, ships its own types, and runs on Node 18 or later (or anywhere with `fetch`). The source is at [github.com/aaquib90/proxymail-qa-js](https://github.com/aaquib90/proxymail-qa-js), and each release is published from GitHub Actions with npm provenance.

Terminal window

```bash
npm install --save-dev proxymail-qa
```

Create an [API key](https://proxymail.qa/docs/api-keys) and set it as `PROXYMAIL_QA_API_KEY`, or pass it in.

## Quick start

```ts
import { ProxyMailQA } from 'proxymail-qa';

const qa = new ProxyMailQA(); // reads PROXYMAIL_QA_API_KEY

const { address, persona, password } = await qa.createAddress({ persona: true, password: true });
const submitted = new Date();
// …sign up in your app with address, persona and password…

const { value: code } = await qa.waitForCode(address, { since: submitted });
// or: const { value: link } = await qa.waitForLink(address, { type: 'verify', since: submitted });

await qa.deleteAddress(address);
```

Waits return the code or link on its own, with the service that sent it and when it expires, never the email’s text. A wait holds for up to 30 seconds and longer `timeout`s repeat it. Full examples: [Playwright](https://proxymail.qa/docs/playwright) and [Cypress](https://proxymail.qa/docs/cypress).

## Methods

| Method                                                                                                                                                                                                                       | What it does                                                                                                                                                                                                                                                                                                                                    |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| new ProxyMailQA({ apiKey?, baseUrl?, fetch?, run? })                                                                                                                                                                         | The client. apiKey defaults to PROXYMAIL\_QA\_API\_KEY, and run to PROXYMAIL\_QA\_RUN                                                                                                                                                                                                                                                           |
| createAddress(options?)                                                                                                                                                                                                      | A new test address. Options: label, name (a custom name before the @), persona, password (returned once, and typed as a string when you pass password: true), lifetimeDays, domain, age or dateOfBirth, country, and where it’s used: site and environment (new in 0.9.0)                                                                       |
| waitForCode(address, options?)                                                                                                                                                                                               | The newest verification or login code. Options: since, service, timeout (seconds, default 30), and unused (skip codes already used: returned by an earlier wait, or copied in the app; new in 0.5.1)                                                                                                                                            |
| waitForLink(address, options?)                                                                                                                                                                                               | The same for a link. Also type (verify, magic\_signin, invite, reset or any, or a list) and app (true for the link that opens a mobile app)                                                                                                                                                                                                     |
| messages(address, options?)                                                                                                                                                                                                  | Emails with their codes and links. includeText and includeHtml add the content, for template tests; limit                                                                                                                                                                                                                                       |
| countEmails(address, options?)                                                                                                                                                                                               | How many emails arrived, each summarised without its content. Options: since, service, type (code or link) and subtype                                                                                                                                                                                                                          |
| expectNoEmail(address, options?)                                                                                                                                                                                             | Resolves if no matching email arrives within timeout seconds (default 30); throws ProxyMailQAError (409, email\_arrived) as soon as one does. Same filters, and since defaults to now                                                                                                                                                           |
| forTest(name), recordTest(result), recordTestTotals(totals)                                                                                                                                                                  | A client that names its test in every call, and test results for the run’s report; proxymail-qa/playwright and its reporter use them. New in 0.8.0 ([more](https://proxymail.qa/docs/ci#which-test-each-email-came-from))                                                                                                                       |
| formValues({ fields, address?, age?, country?, … })                                                                                                                                                                          | What to type or pick in each field of a sign-up form, from a new address made for it. New in 0.7.0 ([more](https://proxymail.qa/docs/node#values-for-a-sign-up-form))                                                                                                                                                                           |
| checkEmail(address, options?)                                                                                                                                                                                                | Checks the newest matching email (or emailId): SPF, DKIM, DMARC, local and plain http links, where its link goes (expectHost), a stated expiry, a plain-text part, size and image-only emails. From 0.7.0, phrases it should contain (expectText) and its language (expectLanguage). Verdicts only, never its content. New in 0.4.0             |
| checkDelivery(address)                                                                                                                                                                                                       | What reached the address and what was refused, without waiting                                                                                                                                                                                                                                                                                  |
| variants(address)                                                                                                                                                                                                            | Unusual versions of the address that still get its mail, and invalid ones ([more](https://proxymail.qa/docs/unusual-addresses))                                                                                                                                                                                                                 |
| getAddress(address), listAddresses({ limit?, before?, tags?, country?, minAge?, maxAge?, hasPassword?, search?, site?, environment? })                                                                                       | Read addresses, or find ones made earlier: every filter given must match                                                                                                                                                                                                                                                                        |
| updateAddress(address, patch)                                                                                                                                                                                                | Change label, notes, tags, first\_name, last\_name, username, date\_of\_birth or country; null clears one                                                                                                                                                                                                                                       |
| setPassword(address, { password?, label? })                                                                                                                                                                                  | Save the password the test account uses now, after the app changed or reset it. Without one, a strong password is generated and returned once                                                                                                                                                                                                   |
| savedPassword(address, label?)                                                                                                                                                                                               | Read a saved password. Needs a key with “Can read saved passwords”; every read is logged                                                                                                                                                                                                                                                        |
| signInAccount({ site?, environment?, tags? })                                                                                                                                                                                | The saved test account to sign in with, and its password, in one call: the one used on this site first; null when none fits. Needs a key with “Can read saved passwords”. New in 0.9.0 ([more](https://proxymail.qa/docs/rest-api#signing-in-again-as-a-test-account))                                                                          |
| markDevHost(host)                                                                                                                                                                                                            | Marks a host such as localhost:3000 as a dev environment of the project, so checkEmail passes links to it. New in 0.9.0                                                                                                                                                                                                                         |
| deleteAddress(address)                                                                                                                                                                                                       | Delete the address and its mail                                                                                                                                                                                                                                                                                                                 |
| project()                                                                                                                                                                                                                    | The key’s project: its mail domains, email prefix and limits                                                                                                                                                                                                                                                                                    |
| checkOutAccount({ tags?, country?, minAge?, maxAge?, hasPassword?, search?, site?, environment?, minutes? }), extendCheckout(account, minutes?), returnAccount(account, { addTags?, removeTags? }), withAccount(options, fn) | The account pool: borrow an account in a known state that no other test has, and give it back. New in 0.6.0 ([more](https://proxymail.qa/docs/rest-api#an-account-pool-for-parallel-tests)). From 0.7.0, withAccount takes create, check and reset hooks: [accounts in a known state](https://proxymail.qa/docs/node#accounts-in-a-known-state) |
| startRun({ name?, source?, meta? }), finishRun({ failed? }), runReport(), addRunNote(text, { failed? }), getRun(), listRuns()                                                                                                | A run: this client’s calls add what they saw, for a report on the pull request. New in 0.5.0                                                                                                                                                                                                                                                    |

Every method takes a plain address or a [variant](https://proxymail.qa/docs/unusual-addresses) of it. Waiting on a `+tag` address only finds mail sent to that tag, so one address can serve a whole test run: see [Running in CI](https://proxymail.qa/docs/ci).

## Values for a sign-up form

`formValues` gives what to type or pick in each field of a form, from a new address made for it: a username and password that fit the fields’ rules, the date of birth in the field’s format, a country the drop-down offers. In Playwright, read the fields from the page and fill each one in:

```ts
const form = page.locator('form#sign-up');
const fields = await form.locator('input, select').evaluateAll((elements) =>
  elements.map((el, i) => {
    const input = el as HTMLInputElement & HTMLSelectElement;
    input.dataset.field = String(i);
    return {
      id: String(i),
      type: input.tagName === 'SELECT' ? 'select' : input.type,
      name: input.name,
      label: input.labels?.[0]?.textContent ?? input.getAttribute('aria-label') ?? '',
      placeholder: input.placeholder,
      autocomplete: input.getAttribute('autocomplete') ?? '',
      pattern: input.pattern || undefined,
      minLength: input.minLength > 0 ? input.minLength : undefined,
      maxLength: input.maxLength > 0 ? input.maxLength : undefined,
      options: input.tagName === 'SELECT' ? Array.from(input.options, (o) => ({ value: o.value, label: o.text })) : undefined,
    };
  }),
);
const { address, values } = await qa.formValues({ fields });
for (const { id, kind, value } of values) {
  if (value === null) continue;
  const field = form.locator(`[data-field="${id}"]`);
  if (await field.evaluate((el) => el.tagName === 'SELECT')) await field.selectOption(value);
  else await field.fill(value);
}
```

Pass `address` to use an account made earlier, and `age` or `country` for a new one. More in the [REST API](https://proxymail.qa/docs/rest-api#values-for-a-sign-up-form) guide.

## Checking that nothing arrives, or one email does

```ts
// An unsubscribed user gets no newsletter.
const sent = new Date();
await sendNewsletter();
await qa.expectNoEmail(unsubscribed.address, { since: sent, timeout: 20 });

// A sign-up sends one verification email, not two.
await qa.waitForLink(address, { since: submitted });
await new Promise((resolve) => setTimeout(resolve, 10_000)); // time for a duplicate
expect((await qa.countEmails(address, { since: submitted, type: 'link' })).count).toBe(1);
```

Neither returns what the emails say, only when each arrived, who sent it and the kind of its code or link. More in the [REST API](https://proxymail.qa/docs/rest-api#checking-that-an-email-didnt-arrive-or-arrived-once) guide.

## Checking an email’s sending and build

```ts
await qa.waitForLink(address, { since: submitted });
const result = await qa.checkEmail(address, { since: submitted, type: 'link', expectHost: 'staging.example.com' });
expect(result.failed).toEqual([]); // e.g. ['local_links'] when the link goes to localhost

// A localised email: the right language, with the user's name in it.
const german = await qa.checkEmail(address, { since: submitted, expectLanguage: 'de', expectText: [persona.first_name] });
expect(german.failed).toEqual([]);
```

Each check passes, fails, warns or is skipped, with a one-line reason; what each means is in the [REST API](https://proxymail.qa/docs/rest-api#checking-an-emails-sending-and-build) guide. Testing an app that runs on your machine? `await qa.markDevHost('localhost:3000')` once, and links to it pass `local_links`; links to any other localhost still fail.

## An account pool for parallel tests

Tag accounts with their state, then let each test borrow one. `withAccount` checks one out, runs your function and always gives it back; return `{ addTags, removeTags }` to record what the test changed:

```ts
await qa.withAccount({ tags: ['verified', 'premium'] }, async (account) => {
  const { secret } = await qa.savedPassword(account.address);
  await signIn(page, account.address, secret);
  await cancelSubscription(page);
  return { addTags: ['cancelled'], removeTags: ['premium'] };
});
```

No other test gets the same account while it’s checked out (10 minutes unless you pass `minutes`), and one that’s never returned frees itself when its time runs out.

### Accounts in a known state

When a test needs an account in a particular state in your app (verified, on the premium plan, with no orders), give `withAccount` three hooks. Your test code runs them with its own access to your app; Proxy Mail QA never signs in to it. Here `app` stands for your own code that talks to your app: an admin API, a database seed or the UI.

```ts
await qa.withAccount(
  {
    tags: ['verified', 'premium'],
    // When none is free: sign up with the new address, verify it, upgrade it.
    create: async (account) => {
      const submitted = new Date();
      await app.signUp(account.address, account.password);
      const { value: link } = await qa.waitForLink(account.address, { type: 'verify', since: submitted });
      await app.open(link);
      await app.admin.setPlan(account.address, 'premium');
    },
    // Is it still in that state? Return false or throw if not.
    check: async (account) => (await app.admin.user(account.address)).plan === 'premium',
    // After the test: put it back.
    reset: async (account) => app.admin.resetUser(account.address, { plan: 'premium', orders: [] }),
  },
  async (account) => {
    // …the test…
  },
);
```

- **check** runs before the test gets the account. If it fails, **reset** runs and it’s checked again; if it still fails, the next account is tried (up to `attempts`, 3), and the one that failed loses the pool’s tags and is tagged `check-failed`.
- **create** runs when no account matches or every match is checked out. The new address is held for this test from the start and gets the tags once create and check succeed. Pass `newAccount` for its age, label or domain; it gets the filter’s country.
- **reset** runs after the test, whether it passed or not. If reset fails, the account loses the pool’s tags and is tagged `needs-reset`, so no other test gets it in an unknown state.

In a [run](https://proxymail.qa/docs/node#a-report-of-the-run), a new account, a failed check and a failed reset each add a line to the report.

## A report of the run

Calls made while a run is open add what they saw to it: the addresses, which emails arrived and how long after `since`, the codes, where each link goes, checks, and emails that never came. In GitHub Actions, the [Proxy Mail QA action](https://proxymail.qa/docs/ci#a-report-on-the-pull-request) starts and finishes the run and posts the report on the pull request; the client picks the run up from `PROXYMAIL_QA_RUN` with no changes to your tests. Elsewhere, run it yourself:

```ts
const run = await qa.startRun({ name: 'Sign-up tests', meta: { branch: process.env.BRANCH } });
// …the tests, using qa…
await qa.finishRun({ failed: testsFailed });
console.log(await qa.runReport(run.id)); // Markdown, for a pull request comment
```

Other processes join the run when you pass them its id as `PROXYMAIL_QA_RUN`.

To show which test each email came from, use a client for each test: `qa.forTest('sign-up › sends one code')` names the test in every call. In Playwright, `proxymail-qa/playwright` gives every test a `qa` fixture that does this, and `proxymail-qa/playwright/reporter` records failed and flaky tests and the totals in the run: see [Running in CI](https://proxymail.qa/docs/ci#which-test-each-email-came-from). Other test runners can send results themselves with `qa.recordTest({ name, status, durationMs, error })` and `qa.recordTestTotals({ passed, failed, skipped, flaky })`. New in 0.8.0.

## Errors

Failed calls throw `ProxyMailQAError` with the HTTP `status`, a `code` and a readable message. When a wait finds nothing, the message also says why as far as Proxy Mail QA can tell (`error.hint`), for example that the app’s email provider only sends to verified addresses, and `error.refused` lists mail turned away for being too large or over a limit:

```ts
import { ProxyMailQAError } from 'proxymail-qa';

try {
  await qa.waitForCode(address, { since: submitted });
} catch (error) {
  if (error instanceof ProxyMailQAError && error.status === 404) console.log(error.hint);
  throw error;
}
```

Retrying won’t help until the app can send to any address. See [Test email not arriving?](https://proxymail.qa/troubleshooting)

## Finding a test account again

Tests and agents can come back to an account they made earlier instead of creating another. Every filter given must match; ages are whole years today, from the persona’s date of birth, and `search` looks in the address, label, notes, name and username.

```ts
// An under-18 in the UK, made for the age-gate tests, with a saved password.
const [teen] = await qa.listAddresses({ tags: ['age-gate'], country: 'GB', maxAge: 17, hasPassword: true });

// Record where an account is used, then sign in with it again later: the
// account used on this site first, with its password (needs a key with
// "Can read saved passwords").
await qa.createAddress({ password: true, site: 'staging.example.com', environment: 'staging' });
const account = await qa.signInAccount({ site: 'staging.example.com', tags: ['verified'] });
```

[Previous REST API](https://proxymail.qa/docs/rest-api.html) [Next Playwright](https://proxymail.qa/docs/playwright.html)
