Email-code sign-in
Some sites email a one-time code (or a sign-in link) after the password. Save the password as a credential whose codes you send yourself (codeSource: "push"). An agent run signs in with %NAME.username%, %NAME.password% and %NAME.otp%; when it reaches the code field it waits. You hear of it (a code step in the run, and the webhook credential.code_needed), read the email, and send the code with credentials.pushCode. The platform types it. The model never sees the password or the code, and a code is used once, by the wait it was sent for.
When to use it: The AI should sign in to a site that emails a one-time code, and your system can read that email and send the code.
| Uses | credentials.create (a password with codeSource: "push"), agent.run with credentials, agent.stream (the code step), credentials.pushCode, credentials.audit |
|---|---|
| Needs | a plan with password credentials (loginDetails), and a model on the API |
| Site | your sign-in page that emails a code (SIGNIN_URL) and a way for your system to read that email (MAILBOX_URL). The runner uses its stand-in: a password page, then a random one-time code "emailed" to a mailbox it serves like a mail provider's API |
| Output | output/result.json with the run's code steps (waiting, received), when the code was sent and the credential's audit (never a value) |
| Languages | Node and Python |
The wait only uses a code that arrives after it began (an older one is never used), so the example reads the mailbox when the run says it waits, not before. A sign-in link works the same way: credentials.pushCode(name, { link }), and the agent's browser_open_link tool opens it (it must be on the credential's sites). To skip the agent and sign in with one call, use session.login(name); it waits for a pushed code in the same way.
The code
The example as it is in its folder. Both versions do the same job and write the same output/, so one result check covers both.
/** * Email-code sign-in: a site that emails a one-time code after the password. The password is a credential with * `codeSource: "push"`: an agent run signs in with %NAME.username%, %NAME.password% and %NAME.otp%, and when it reaches * the code field it waits. You hear of it (a `code` step in the run, and the webhook `credential.code_needed`), read * the email, and send the code with `credentials.pushCode`. The model never sees the password or the code: the * platform types them, only on that site. * * SIGNIN_URL=https://… SITE_USERNAME=… SITE_PASSWORD=… MAILBOX_URL=https://… npx tsx node/index.ts (BOXLINE_API_KEY) * * MAILBOX_URL is where YOUR system reads the site's email: an address that answers `{"messages": [{"text": "...", * "receivedAt": "<iso time>"}]}`, newest first (here it is the stand-in site's mailbox, like a mail provider's API). * With a real site, change readCode() to read your own mailbox. CREDENTIAL_NAME picks the credential's name (default * EMAIL_CODE_EXAMPLE). Needs a plan with password credentials (loginDetails). Writes output/result.json. */import { mkdirSync, writeFileSync } from "node:fs";import { join } from "node:path";import { setTimeout as sleep } from "node:timers/promises";import { Boxline, CredentialExistsError } from "@boxline/sdk";const out = process.env.OUTPUT_DIR ?? "output";const { SIGNIN_URL: signin, SITE_USERNAME: username, SITE_PASSWORD: password, MAILBOX_URL: mailbox } = process.env;if (!signin || !username || !password || !mailbox) throw new Error("Set SIGNIN_URL, SITE_USERNAME, SITE_PASSWORD and MAILBOX_URL");const name = process.env.CREDENTIAL_NAME ?? "EMAIL_CODE_EXAMPLE";const bx = new Boxline();/** The newest 6-digit code in a message that arrived after `since` (ms): the one function to change for your mailbox. */async function readCode(since: number): Promise<string> { for (let attempt = 0; attempt < 60; attempt++) { const { messages } = (await fetch(mailbox!).then((r) => r.json())) as { messages: { text: string; receivedAt: string }[] }; const mail = messages.find((m) => Date.parse(m.receivedAt) >= since); const code = mail?.text.match(/\b(\d{6})\b/)?.[1]; if (code) return code; await sleep(1000); } throw new Error("no email with a code arrived in the mailbox");}// 1. The password as a credential whose codes you send yourself (sealed on the platform; never shown again).const origins = [new URL(signin).origin];const fields = { origins, username, password, codeSource: "push" as const, codeTimeoutSeconds: 120 };let credentialMade = false;try { await bx.credentials.create({ name, type: "password", ...fields }); credentialMade = true;} catch (err) { if (!(err instanceof CredentialExistsError)) throw err; await bx.credentials.update(name, fields);}const session = await bx.sessions.create({ timeout: 600, idleTimeout: 300, userMetadata: { example: "email-code-sign-in" } });console.log(`Session: ${session.id}`);let result: Record<string, unknown>;try { // 2. The run signs in. At the code field it types %NAME.otp%, and the platform waits for a code that you send. const started = Date.now() - 2000; const run = await bx.agent.run({ sessionId: session.id, credentials: [name], maxSteps: 25, task: `Open ${signin} and sign in: type %${name}.username% as the email and %${name}.password% as the password, then press Next. ` + `The site emails a sign-in code: type %${name}.otp% into the code field and press Verify. Then tell me the name shown on the page.`, }); console.log(`Agent run ${run.id} (${run.provider}/${run.model})`); // 3. When the run waits for the code, read the email and send the code. It is used once, by this wait, and the run // never holds it. let pushedAt: number | null = null; for await (const e of bx.agent.stream(run.id)) { if (e.type === "tool") console.log(`→ ${e.name} ${JSON.stringify(e.input ?? {}).slice(0, 100)}`); // placeholders only else if (e.type === "code" && e.state === "waiting") { console.log(`The run waits for the ${e.kind} the site emailed for ${e.credential}: reading the mailbox…`); const code = await readCode(started); pushedAt = Date.now(); await bx.credentials.pushCode(name, { code }); console.log(`Sent the ${code.length}-digit code to ${name}`); } else if (e.type === "done") console.log(`\n${e.status}: ${e.result ?? e.error}`); } const done = await bx.agent.get(run.id); const audit: { action: string; kind?: unknown }[] = []; for await (const x of bx.credentials.audit({ name })) audit.push({ action: x.action, ...(x.details?.kind ? { kind: x.details.kind } : {}) }); result = { signinUrl: signin, credential: name, agentRuns: [{ id: run.id }], status: done.status, answer: done.result, steps: done.steps.length, // What the run showed while it waited: waiting, then received (never the code). codeSteps: done.steps.filter((s) => s.type === "code").map((s) => s.state), pushedAt, audit, made: { credentials: credentialMade ? [name] : [] }, };} finally { await session.stop(); if (credentialMade) await bx.credentials.delete(name).catch(() => undefined);}mkdirSync(out, { recursive: true });writeFileSync(join(out, "result.json"), JSON.stringify(result, null, 2));"""Email-code sign-in: a site that emails a one-time code after the password. The password is a credential with``code_source="push"``: an agent run signs in with %NAME.username%, %NAME.password% and %NAME.otp%, and when it reachesthe code field it waits. You hear of it (a ``code`` step in the run, and the webhook ``credential.code_needed``), readthe email, and send the code with ``credentials.push_code``. The model never sees the password or the code: theplatform types them, only on that site. SIGNIN_URL=https://… SITE_USERNAME=… SITE_PASSWORD=… MAILBOX_URL=https://… python python/main.py (BOXLINE_API_KEY)MAILBOX_URL is where YOUR system reads the site's email: an address that answers ``{"messages": [{"text": "...","receivedAt": "<iso time>"}]}``, newest first (here it is the stand-in site's mailbox, like a mail provider's API). With areal site, change read_code() to read your own mailbox. CREDENTIAL_NAME picks the credential's name (defaultEMAIL_CODE_EXAMPLE). Needs a plan with password credentials (loginDetails). Writes output/result.json."""import jsonimport osimport reimport timeimport urllib.requestfrom datetime import datetimefrom pathlib import Pathfrom urllib.parse import urlparsefrom boxline import Boxline, CredentialExistsErrorout = Path(os.environ.get("OUTPUT_DIR", "output"))signin, username, password, mailbox = (os.environ.get(k) for k in ("SIGNIN_URL", "SITE_USERNAME", "SITE_PASSWORD", "MAILBOX_URL"))if not (signin and username and password and mailbox): raise SystemExit("Set SIGNIN_URL, SITE_USERNAME, SITE_PASSWORD and MAILBOX_URL")name = os.environ.get("CREDENTIAL_NAME", "EMAIL_CODE_EXAMPLE")bx = Boxline()def read_code(since: float) -> str: """The newest 6-digit code in a message that arrived after `since` (seconds): the one function to change for your mailbox.""" for _ in range(60): with urllib.request.urlopen(mailbox, timeout=10) as r: messages = json.load(r)["messages"] for m in messages: if datetime.fromisoformat(m["receivedAt"].replace("Z", "+00:00")).timestamp() >= since: found = re.search(r"\b(\d{6})\b", m["text"]) if found: return found.group(1) time.sleep(1) raise SystemExit("no email with a code arrived in the mailbox")# 1. The password as a credential whose codes you send yourself (sealed on the platform; never shown again).u = urlparse(signin)fields = {"origins": [f"{u.scheme}://{u.netloc}"], "username": username, "password": password, "code_source": "push", "code_timeout_seconds": 120}credential_made = Falsetry: bx.credentials.create(name, "password", **fields) credential_made = Trueexcept CredentialExistsError: bx.credentials.update(name, **fields)result = Nonetry: with bx.sessions.create(timeout=600, idle_timeout=300, user_metadata={"example": "email-code-sign-in"}) as session: print(f"Session: {session.id}", flush=True) # 2. The run signs in. At the code field it types %NAME.otp%, and the platform waits for a code that you send. started = time.time() - 2 run = bx.agent.run( f"Open {signin} and sign in: type %{name}.username% as the email and %{name}.password% as the password, then press Next. " f"The site emails a sign-in code: type %{name}.otp% into the code field and press Verify. Then tell me the name shown on the page.", session_id=session.id, credentials=[name], max_steps=25, ) print(f"Agent run {run['id']} ({run['provider']}/{run['model']})", flush=True) # 3. When the run waits for the code, read the email and send the code. It is used once, by this wait, and the # run never holds it. pushed_at = None for e in bx.agent.stream(run["id"]): if e["type"] == "tool": print(f"-> {e['name']} {json.dumps(e.get('input') or {})[:100]}") # placeholders only elif e["type"] == "code" and e.get("state") == "waiting": print(f"The run waits for the {e.get('kind')} the site emailed for {e.get('credential')}: reading the mailbox…", flush=True) code = read_code(started) pushed_at = int(time.time() * 1000) bx.credentials.push_code(name, code=code) print(f"Sent the {len(code)}-digit code to {name}", flush=True) elif e["type"] == "done": print(f"\n{e['status']}: {e.get('result') or e.get('error')}") done = bx.agent.get(run["id"]) audit = [{"action": x["action"], **({"kind": x["details"]["kind"]} if x.get("details", {}).get("kind") else {})} for x in bx.credentials.audit(name=name)] result = { "signinUrl": signin, "credential": name, "agentRuns": [{"id": run["id"]}], "status": done["status"], "answer": done["result"], "steps": len(done["steps"]), # What the run showed while it waited: waiting, then received (never the code). "codeSteps": [s.get("state") for s in done["steps"] if s["type"] == "code"], "pushedAt": pushed_at, "audit": audit, "made": {"credentials": [name] if credential_made else []}, }finally: if credential_made: try: bx.credentials.delete(name) except Exception: passout.mkdir(parents=True, exist_ok=True)(out / "result.json").write_text(json.dumps(result, indent=2, ensure_ascii=False))Inputs
Environment variables the example reads:
SIGNIN_URL: your sign-in page (required).SITE_USERNAME: your user name or email.SITE_PASSWORD: your password (keep it in the environment, never in code).MAILBOX_URL: where your system reads the site's email: an address that answers{"messages": [{"text": "...", "receivedAt": "<iso time>"}]}, newest first. The example takes the first 6-digit number of the newest message that arrived after it started. With a real site, changereadCode()(read_code()in Python) to read your own mailbox, or send the code from the handler of an inbound-email webhook instead.CREDENTIAL_NAME: the credential's name (defaultEMAIL_CODE_EXAMPLE).
Run it
Get the examples with git clone https://github.com/Boxline-dev/examples.git and open this one: cd examples/email-code-sign-in. Put your API key in BOXLINE_API_KEY (and BOXLINE_API_URL for another API), then run either version from the example’s folder. Both write to output/ and stop their sessions when they finish.
Both write to output/ (OUTPUT_DIR picks another folder), stop their session and delete the credential they made when they finish.
# Node 18 or newer, in the example's folderexport BOXLINE_API_KEY=… # your API keynpm install @boxline/sdk tsxnpx tsx node/index.ts# Python 3.9 or newer, in the example's folderexport BOXLINE_API_KEY=… # your API keypip install boxline-sdkpython python/main.pyThen check the result, from the same folder: npx tsx check.ts output. It exits with 0 only when the result is right.
What its check proves
check.ts reads what the example wrote and passes only when the result is right, not merely when the example ran.
On the stand-in, the site saw the password, emailed a code, and then got that same code typed after the example sent it; the run's code steps were waiting then received; the answer names the signed-in user; and neither the stored run, the credential's audit nor the result holds the code or the password.
Source
github.com/Boxline-dev/examples/email-code-sign-in on GitHub, with its README.md, check.ts and .env.example. The examples are open source. See all examples.
These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.