# Migrate from Mailosaur

Map Mailosaur messages.get, search criteria, codes and links to the InboxSpec wait.

<a id="guide-content"></a>



Mailosaur's client waits by searching repeatedly until a message appears. InboxSpec's wait is one request that the server holds open until a message matches or the timeout passes. Most tests change in three places: where the address comes from, the wait call, and how codes and links are read. Mailosaur behaviour described here was checked on 24 October 2026; see the [Mailosaur comparison](https://www.inboxspec.com/docs/markdown/compare-mailosaur.md) for sources.

<a id="before-and-after"></a>

## Before and after

A Node.js signup test using Mailosaur's client:

```javascript
import MailosaurClient from "mailosaur";

const mailosaur = new MailosaurClient(process.env.MAILOSAUR_API_KEY);
const serverId = process.env.MAILOSAUR_SERVER_ID;
const emailAddress = `signup-${Date.now()}@${serverId}.mailosaur.net`;

await startSignup(emailAddress); // your application sends the email

const message = await mailosaur.messages.get(
  serverId,
  { sentTo: emailAddress, subject: "Verify" },
  { timeout: 30000 },
);
const code = message.html.codes[0].value;
const link = message.html.links[0].href;
```

The same test with InboxSpec's REST API and Node.js 18 or later. Set `INBOXSPEC_API_KEY` to a project key first.

```javascript
const origin = process.env.INBOXSPEC_API_URL ?? "https://api.inboxspec.com";

async function inboxspec(path, body) {
  const response = await fetch(origin + path, {
    method: "POST",
    headers: {
      Authorization: "Bearer " + process.env.INBOXSPEC_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });
  // Authentication, quota and validation problems are HTTP errors, never timeouts.
  if (!response.ok) throw new Error(`InboxSpec ${response.status}: ${await response.text()}`);
  return response.json();
}

const inbox = await inboxspec("/v1/inboxes/for-run", { run_id: `signup-${Date.now()}` });

await startSignup(inbox.address); // your application sends the email

const result = await inboxspec("/v1/messages/wait", {
  inbox: inbox.address,
  match: { subject_contains: "Verify", has_code: true },
  timeout_ms: 30000,
});
if (result.status === "timeout") throw new Error(result.hint);
const code = result.message.codes[0];
const link = result.message.links.find((candidate) => candidate.kind === "verify")?.href;
```

Register the inbox before your application sends to it. Mail to an address that was never registered is not stored.

<a id="call-mapping"></a>

## Call mapping

| Mailosaur | InboxSpec |
| --- | --- |
| API key as the Basic-auth username | `Authorization: Bearer` with a project key |
| A server, with any address under its subdomain | A project, with one registered inbox per run from `POST /v1/inboxes/for-run` |
| `messages.get(serverId, criteria, options)` | `POST /v1/messages/wait` with `inbox`, `match` and `timeout_ms` |
| `sentTo` | The `inbox` itself, or `match.to_contains` |
| `sentFrom` | `match.from_contains` |
| `subject` | `match.subject_contains` |
| `body` | `match.body_contains`, which searches the subject and plain-text part only |
| `match: "ANY"` | No equivalent. Match fields are always combined with AND, so issue one wait per alternative |
| `receivedAfter`, which defaults to one hour ago | `match.since`, which has no default; every stored message in the inbox is eligible |
| `timeout`, 10 seconds by default, counted in the client | `timeout_ms`, 120,000 by default and at most 285,000, counted on the server |
| `errorOnTimeout: false` | Always on: a timeout returns `status: "timeout"` with a hint |
| `message.html.codes[0].value` | `result.message.codes[0]`, best candidate first |
| `message.html.links[0].href` | `result.message.links`, each with `href`, `text` and `kind` |
| `message.html.body` | `GET /v1/messages/:id/body?format=html` |
| `messages.search` and `messages.list` | `GET /v1/messages?inbox=…` |
| `messages.del` and `messages.deleteAll` | `DELETE /v1/messages/:id`, `DELETE /v1/inboxes/:id` or `POST /v1/inboxes/cleanup` |
| SMTP settings from the inbox's Connect tab | [Capture SMTP](https://www.inboxspec.com/docs/markdown/capture-smtp.md) credentials, port 587 with STARTTLS |

<a id="behaviour-to-check"></a>

## Behaviour to check after switching

-   A wait does not consume messages, and it returns the oldest match. Use a fresh run ID for each journey rather than relying on a time window. See [Inboxes](https://www.inboxspec.com/docs/markdown/inboxes.md).
-   Code values are ranked candidates. Assert the one your application issued; see [OTP and link assertions](https://www.inboxspec.com/docs/markdown/otp-tests.md).
-   Keep your HTTP client's timeout longer than `timeout_ms`, so the server's answer arrives before your client gives up.
-   InboxSpec does not include Mailosaur's compatibility, accessibility, SMS or preview testing. Keep those tests where they are.

Next, follow the [CI guide](https://www.inboxspec.com/docs/markdown/ci.md) for fixtures, assertions and cleanup in a pipeline.
