LumboxLumbox Docs

Steel

Connect a Steel cloud browser session to a Lumbox inbox.

Steel

Use Steel's Sessions API with Playwright to complete an email verification flow using Lumbox. The example creates the inbox, opens a remote browser session, submits a signup form, reads the email code, and checks a site-specific success selector.

By Kumar

Install and configure

The example was checked with steel-sdk 0.18.0 and playwright 1.63.0. Steel's SDK type for sessions.create() does not include inactivityTimeout, so this example creates the session through the documented Sessions REST endpoint and uses steel-sdk to release it.

npm install steel-sdk@0.18.0 playwright@1.63.0 tsx@4.23.15 typescript@7.0.2
export LUMBOX_API_KEY=ak_your_key
export STEEL_API_KEY=your_steel_key
export SIGNUP_URL=https://your-site.example/signup
export EXPECTED_SENDER=accounts@your-site.example
export EMAIL_SELECTOR='input[name="email"]'
export SIGNUP_SUBMIT_SELECTOR='button[type="submit"]'
export OTP_SELECTOR='input[name="verificationCode"]'
export VERIFY_SUBMIT_SELECTOR='button[type="submit"]'
export SUCCESS_SELECTOR='[data-testid="account-dashboard"]'

The URL, selectors, and success condition must match a site you control or are authorized to automate. EXPECTED_SENDER is a substring filter, not sender authentication. Run as npx tsx steel.ts.

Create and verify an account

import Steel from "steel-sdk";
import { chromium, type Browser } from "playwright";

const required = (name: string): string => {
  const value = process.env[name];
  if (!value) throw new Error(`${name} is required`);
  return value;
};

const apiKey = required("LUMBOX_API_KEY");
const steelApiKey = required("STEEL_API_KEY");
const signupUrl = required("SIGNUP_URL");
const expectedSender = required("EXPECTED_SENDER");
const emailSelector = required("EMAIL_SELECTOR");
const signupSubmitSelector = required("SIGNUP_SUBMIT_SELECTOR");
const otpSelector = required("OTP_SELECTOR");
const verifySubmitSelector = required("VERIFY_SUBMIT_SELECTOR");
const successSelector = required("SUCCESS_SELECTOR");
const apiUrl = process.env.LUMBOX_API_URL ?? "https://api.lumbox.co";
const steel = new Steel({ steelAPIKey: steelApiKey });

async function createInbox() {
  const response = await fetch(`${apiUrl}/v1/inboxes`, {
    method: "POST",
    headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
    body: JSON.stringify({}),
  });
  if (!response.ok) throw new Error(`Lumbox inbox creation failed with HTTP ${response.status}`);
  const inbox = await response.json() as { id: string; address: string };
  if (!inbox.id || !inbox.address) throw new Error("Lumbox returned an incomplete inbox response");
  return inbox;
}

async function getOtp(inboxId: string, since: string): Promise<string> {
  const query = new URLSearchParams({ timeout: "30", since, from: expectedSender });
  const response = await fetch(`${apiUrl}/v1/inboxes/${encodeURIComponent(inboxId)}/otp?${query}`, {
    headers: { "X-API-Key": apiKey },
  });
  if (response.status === 408) throw new Error("No matching verification code arrived within 30 seconds");
  if (!response.ok) throw new Error(`Lumbox OTP request failed with HTTP ${response.status}`);
  const result = await response.json() as { code?: string };
  if (typeof result.code !== "string" || result.code.length === 0) throw new Error("Lumbox returned no verification code");
  return result.code;
}

async function createSession(): Promise<{ id: string; websocketUrl: string }> {
  const response = await fetch("https://api.steel.dev/v1/sessions", {
    method: "POST",
    headers: { "steel-api-key": steelApiKey, "Content-Type": "application/json" },
    body: JSON.stringify({ timeout: 240_000, inactivityTimeout: 90_000 }),
  });
  if (!response.ok) throw new Error(`Steel session creation failed with HTTP ${response.status}`);
  return await response.json() as { id: string; websocketUrl: string };
}

async function useSteelSession<T>(
  session: { id: string; websocketUrl: string },
  apiKey: string,
  connect: (url: string) => Promise<Browser>,
  run: (browser: Browser) => Promise<T>,
  release: (id: string) => Promise<unknown>,
): Promise<T> {
  let browser: Browser | undefined;
  try {
    const connectionUrl = new URL(session.websocketUrl);
    connectionUrl.searchParams.set("apiKey", apiKey);
    browser = await connect(connectionUrl.toString());
    return await run(browser);
  } finally {
    try {
      await browser?.close();
    } finally {
      await release(session.id);
    }
  }
}

async function main() {
  const inbox = await createInbox();
  const session = await createSession();
  await useSteelSession(
    session,
    steelApiKey,
    (url) => chromium.connectOverCDP(url),
    async (browser) => {
      const context = browser.contexts()[0];
      const page = context?.pages()[0];
      if (!context || !page) throw new Error("Steel session did not provide a browser context and page");

      await page.goto(signupUrl);
      await page.locator(emailSelector).fill(inbox.address);
      const since = new Date().toISOString();
      await page.locator(signupSubmitSelector).click();

      const code = await getOtp(inbox.id, since);
      await page.locator(otpSelector).fill(code);
      await page.locator(verifySubmitSelector).click();
      await page.locator(successSelector).waitFor({ state: "visible", timeout: 30_000 });
      console.log("Signup completed and the configured success selector is visible.");
    },
    (id) => steel.sessions.release(id),
  );
}

main().catch((error: unknown) => {
  console.error(error instanceof Error ? error.message : "Signup failed");
  process.exitCode = 1;
});

Steel returns a websocketUrl and an existing browser context. The API key is added with URL parsing before chromium.connectOverCDP(). The session allows 90 seconds without activity and has a four minute hard limit, so its idle window exceeds the 30 second Lumbox OTP wait. The code reuses the session's first context and page.

Lumbox behavior and cleanup

The inbox address is read from Lumbox's create response, and its default domain follows account configuration. since is captured immediately before submitting the signup form and reused in the OTP request. from is a substring filter. The endpoint returns the first code extracted from the newest matching verification email as a string. It does not consume the code or check the site's expiry rules.

The OTP endpoint defaults to 30 seconds and accepts 1 through 120 seconds. A 408 response means no matching code arrived. A 401 indicates authentication failure, 403 indicates denied inbox access, and 402 during creation indicates the inbox limit. Other failures are reported with their HTTP status.

The finally block closes Playwright and releases the Steel session even if the connection or signup fails. The Lumbox inbox remains available as a persistent identity. Manually delete it with DELETE /v1/inboxes/:id when finished; deletion permanently removes the inbox and its emails.

Troubleshooting

  • A Playwright connection failure can indicate an unavailable Steel session or an invalid WebSocket URL. The session release still runs after a failed connection.
  • A 408 from Lumbox means the sender substring or signup site's delivery behavior needs checking.
  • A success selector timeout means the verification submit did not reach the site-specific success state.
  • Steel's cloud session uses an inactivity timer and a hard timeout. Choose larger values if the site's flow can take longer than the example.

Sources checked