Testing
OTP and link assertions
Correlate verification codes and reset links with the exact application journey.
OTP login, correlation and bounded waits
This SDK recipe uses application-specific trigger and completion functions. Set a total test deadline longer than the wait and its transport margin. Configure the application to send through the fixture domain’s MX route: authenticated Capture SMTP files mail under separate capture inboxes.
import { InboxSpec } from "@inboxspec/sdk";
const client = new InboxSpec({
apiKey: process.env.INBOXSPEC_API_KEY!,
baseUrl: process.env.INBOXSPEC_API_URL!,
});
const runId = "ci.4182.a1.w1"; // Supply your run, attempt and worker IDs.
const correlationId = runId + ".signup";
try {
const setup = await client.fixtures.ensure({
run_id: runId,
manifest: { version: 1, fixtures: [
{ name: "signup", retention_seconds: 600 },
] },
});
if (setup.status !== "ready") throw new Error("Fixture setup failed");
const inbox = setup.fixtures.find((f) => f.name === "signup")!.inbox!;
const since = new Date().toISOString();
// Your application sends X-InboxSpec-Correlation-ID: correlationId.
await triggerYourApplication(inbox.address, correlationId);
const outcome = await client.messages.wait({
inbox: inbox.id,
match: { since, correlation_id: correlationId, has_code: true },
request_id: "wait-" + correlationId,
timeout_ms: 60_000,
});
if (outcome.status === "timeout") throw new Error(outcome.reason);
const code = outcome.message.codes[0];
if (!code) throw new Error("No verification code");
await completeYourApplicationFlow(code);
} finally {
const cleanup = await client.fixtures.cleanup(runId);
if (cleanup.status !== "complete") throw new Error("Cleanup incomplete");
}If the sender cannot set correlation headers, use a fresh inbox per journey and narrow with since, subject_contains and from_contains. to_contains matches headers and may omit BCC recipients; the selected inbox is the envelope-delivery boundary. correlation_id exactly matches X-InboxSpec-Correlation-ID, case-sensitively. For password reset or verification links, use has_link_kind with reset_password or verify and validate the application origin before following a link.
Timeouts return HTTP 200 with status: timeout and reason: no_arrivals or no_matching_arrivals. Check the hint and arrival count. Authentication, quota, concurrency and infrastructure errors remain separate. Responses include started_at, completed_at, waited_ms and request_id; matched mail includes received_at, measured at SMTP DATA completion.
If workspace receiving is blocked and no stored message matches, waits return HTTP 402 with organisation_limit_exceeded. For an exhausted message allowance, read details.used, limit and period_resets_at. Pause the sending batch and review workspace usage before retrying. Existing mail remains readable; deleting it does not reset monthly usage. Before creating test accounts, use GET /v1/usage with usage:read scope to inspect the effective organisation allowance and extra-usage settings.
Request IDs trace attempts; they are not durable replay keys. Waits do not consume messages, so repeated waits can return the same oldest match. Use a fresh namespace for a new test attempt and reuse a run only for setup retries. Avoid polling loops. Honor retry guidance within one deadline; use SDK signal or CLI Ctrl+C to cancel.
inboxspec fixtures cleanup --run ci.4182.a1.w1 --jsonRun cleanup after producers and waits finish, in finally or a CI always() step. It uses both the run prefix and exact reserved run label, preventing deletion of nested runs. Incomplete cleanup is nonzero and can be resumed with the same run ID. Expiry is a fallback; physical removal may lag. Cleanup does not reset usage or delete accounting history.