# OTP and link assertions

Correlate verification codes and reset links with the exact application journey.

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



<a id="test-architecture"></a>

### 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.

```typescript
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.

```bash
inboxspec fixtures cleanup --run ci.4182.a1.w1 --json
```

Run 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.
