Log In via Email OTP
Automate login flows that send a one-time passcode (OTP) to an email address — navigate the login page, trigger the OTP email, read the code, and enter it.
- A Browserless API token from your account dashboard
- An email inbox you can query programmatically (e.g. Mailosaur, Mailslurp, or your own IMAP server)
Steps
The flow has three stages: trigger the OTP email, read the code from the inbox, then enter it in the browser. The examples below show the browser automation steps — substitute getOtpFromInbox() with whichever email API you use.
- Frameworks
- BQL
Connect a browser to trigger the OTP email, wait for the OTP field to appear, then enter the code.
- Puppeteer
- Playwright
1. Install dependencies
npm install puppeteer-core
2. Trigger OTP, read code, enter it
import puppeteer from 'puppeteer-core';
// Swap this stub with your actual inbox API — see the Inbox integrations table below.
async function getOtpFromInbox(email) {
throw new Error('Implement getOtpFromInbox() with your email provider');
}
const browser = await puppeteer.connect({
browserWSEndpoint: 'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE',
});
try {
const page = await browser.newPage();
// Submit the email to trigger the OTP — the form changes state before the OTP field appears.
await page.goto('https://app.example.com/login', { waitUntil: 'networkidle2' });
await page.type('input[type="email"]', 'user@example.com');
await page.click('button[type="submit"]');
await page.waitForSelector('input[name="otp"], input[autocomplete="one-time-code"]');
// Poll the inbox after the OTP field appears, not before — the email may not be sent yet.
const otp = await getOtpFromInbox('user@example.com');
console.log('Got OTP:', otp);
await page.type('input[name="otp"], input[autocomplete="one-time-code"]', otp);
await page.click('button[type="submit"]');
await page.waitForNavigation({ waitUntil: 'networkidle2' });
console.log('Logged in. URL:', page.url());
} finally {
// Always close to release the session even on error.
await browser.close();
}
3. Check the output
Run with node otp.mjs. Once you supply an getOtpFromInbox() implementation, the script completes the full email OTP login flow.
- JavaScript
- Python
- Java
- C#
1. Install dependencies
npm install playwright-core
2. Trigger OTP, read code, enter it
import { chromium } from 'playwright-core';
// Swap this stub with your actual inbox API — see the Inbox integrations table below.
async function getOtpFromInbox(email) {
throw new Error('Implement getOtpFromInbox() with your email provider');
}
const browser = await chromium.connectOverCDP(
'wss://production-sfo.browserless.io?token=YOUR_API_TOKEN_HERE'
);
try {
// Use the default context — browser.newPage() creates a new context that
// doesn't inherit proxy, profile, or launch settings.
const context = browser.contexts()[0];
const page = await context.newPage();
// Submit the email to trigger the OTP — the form changes state before the OTP field appears.
await page.goto('https://app.example.com/login', { waitUntil: 'networkidle' });
await page.fill('input[type="email"]', 'user@example.com');
await page.click('button[type="submit"]');
await page.waitForSelector('input[name="otp"], input[autocomplete="one-time-code"]');
// Poll the inbox after the OTP field appears, not before — the email may not be sent yet.
const otp = await getOtpFromInbox('user@example.com');
console.log('Got OTP:', otp);
await page.fill('input[name="otp"], input[autocomplete="one-time-code"]', otp);
await page.click('button[type="submit"]');
await page.waitForLoadState('networkidle');
console.log('Logged in. URL:', page.url());
} finally {
// Always close to release the session even on error.
await browser.close();
}
3. Check the output
Run with node otp.mjs. Supply your inbox polling implementation to complete the flow end-to-end.
1. Install dependencies
pip install playwright
2. Trigger OTP, read code, enter it
from playwright.sync_api import sync_playwright
def get_otp_from_inbox(email: str) -> str:
# Swap this stub with your actual inbox API — see the Inbox integrations table below.
raise NotImplementedError('Implement get_otp_from_inbox() with your email provider')
TOKEN = 'YOUR_API_TOKEN_HERE'
WS_ENDPOINT = f'wss://production-sfo.browserless.io/chromium/playwright?token={TOKEN}'
with sync_playwright() as playwright:
browser = playwright.chromium.connect(WS_ENDPOINT)
try:
context = browser.new_context()
page = context.new_page()
# Submit the email to trigger the OTP — the form changes state before the OTP field appears.
page.goto('https://app.example.com/login')
page.fill('input[type="email"]', 'user@example.com')
page.click('button[type="submit"]')
page.wait_for_selector('input[name="otp"], input[autocomplete="one-time-code"]')
# Poll the inbox after the OTP field appears, not before — the email may not be sent yet.
otp = get_otp_from_inbox('user@example.com')
print(f'Got OTP: {otp}')
page.fill('input[name="otp"], input[autocomplete="one-time-code"]', otp)
page.click('button[type="submit"]')
page.wait_for_load_state('networkidle')
print(f'Logged in. URL: {page.url}')
finally:
# Always close to release the session even on error.
browser.close()
3. Check the output
Run with python otp.py. Supply your inbox polling implementation to complete the flow end-to-end.
1. Add dependencies
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.44.0</version>
</dependency>
2. Trigger OTP, read code, enter it
import com.microsoft.playwright.*;
public class OtpLogin {
static String getOtpFromInbox(String email) {
// Swap this stub with your actual inbox API — see the Inbox integrations table below.
throw new UnsupportedOperationException("Implement getOtpFromInbox() with your email provider");
}
public static void main(String[] args) {
String token = "YOUR_API_TOKEN_HERE";
String wsEndpoint = "wss://production-sfo.browserless.io?token=" + token;
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().connectOverCDP(wsEndpoint);
try {
// Reuse the existing page from the default context so Browserless CDP
// events are visible on this page object.
BrowserContext context = browser.contexts().get(0);
Page page = context.pages().get(0);
// Submit the email to trigger the OTP — the form changes state before the OTP field appears.
page.navigate("https://app.example.com/login");
page.fill("input[type='email']", "user@example.com");
page.click("button[type='submit']");
page.waitForSelector("input[name='otp'], input[autocomplete='one-time-code']");
// Poll the inbox after the OTP field appears, not before — the email may not be sent yet.
String otp = getOtpFromInbox("user@example.com");
System.out.println("Got OTP: " + otp);
page.fill("input[name='otp'], input[autocomplete='one-time-code']", otp);
page.click("button[type='submit']");
page.waitForLoadState(LoadState.NETWORKIDLE);
System.out.println("Logged in. URL: " + page.url());
} finally {
// Always close to release the session even on error.
browser.close();
}
}
}
}
3. Check the output
Run with mvn exec:java. Supply your inbox polling implementation to complete the flow end-to-end.
1. Add dependencies
dotnet add package Microsoft.Playwright
2. Trigger OTP, read code, enter it
using Microsoft.Playwright;
static string GetOtpFromInbox(string email)
{
// Swap this stub with your actual inbox API — see the Inbox integrations table below.
throw new NotImplementedException("Implement GetOtpFromInbox() with your email provider");
}
const string Token = "YOUR_API_TOKEN_HERE";
const string WsEndpoint = $"wss://production-sfo.browserless.io?token={Token}";
using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectOverCDPAsync(WsEndpoint);
try
{
// Reuse the existing page from the default context so Browserless CDP
// events are visible on this page object.
var context = browser.Contexts[0];
var page = context.Pages[0];
// Submit the email to trigger the OTP — the form changes state before the OTP field appears.
await page.GotoAsync("https://app.example.com/login");
await page.FillAsync("input[type='email']", "user@example.com");
await page.ClickAsync("button[type='submit']");
await page.WaitForSelectorAsync("input[name='otp'], input[autocomplete='one-time-code']");
// Poll the inbox after the OTP field appears, not before — the email may not be sent yet.
var otp = GetOtpFromInbox("user@example.com");
Console.WriteLine($"Got OTP: {otp}");
await page.FillAsync("input[name='otp'], input[autocomplete='one-time-code']", otp);
await page.ClickAsync("button[type='submit']");
await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
Console.WriteLine($"Logged in. URL: {page.Url}");
}
finally
{
// Always close to release the session even on error.
await browser.CloseAsync();
}
3. Check the output
Run with dotnet run. Supply your inbox polling implementation to complete the flow end-to-end.
1. Write the first mutation
Include reconnect at the end of the first mutation. Without it, the session closes as soon as the mutation completes and there is nothing to send the second mutation to. reconnect keeps the browser alive and returns a browserQLEndpoint URL for the follow-up request:
reconnect(timeout: 60000)requires at least a Starter plan — the Free plan caps reconnect timeouts at 10 seconds. Scale plans support up to 5 minutes.
mutation TriggerOTP {
goto(url: "https://app.example.com/login") {
status
}
typeEmail: type(selector: "input[type=email]", text: "user@example.com") {
time
}
submit: click(selector: "button[type=submit]") {
time
}
waitForOTPField: waitForSelector(
selector: "input[name=otp], input[autocomplete=one-time-code]"
) {
selector
}
reconnect(timeout: 60000) {
browserQLEndpoint
}
}
2. Poll your inbox, then send the second mutation
The TriggerOTP response includes data.reconnect.browserQLEndpoint. Append ?token=YOUR_API_TOKEN_HERE to that URL and POST EnterOTP to it:
mutation EnterOTP {
typeOTP: type(
selector: "input[name=otp], input[autocomplete=one-time-code]"
text: "YOUR_OTP_HERE"
) {
time
}
submit: click(selector: "button[type=submit]") {
time
}
waitForNavigation {
status
}
}
3. Run it
Paste the first mutation into the BQL IDE and click Run, or send it via HTTP. After getting the OTP from your inbox, POST the second mutation to the returned browserQLEndpoint.
Inbox integrations
| Service | How to get the OTP |
|---|---|
| Mailosaur | Use the Mailosaur Node/Python SDK — client.messages.get(serverId, ...) |
| Mailslurp | inbox.waitForLatestEmail() with a regex to extract the code |
| Gmail API | Search for the OTP email subject and parse the body |
| IMAP | Use imaplib (Python) or imap-simple (Node.js) to fetch the latest message |
Next steps
- Save Logins to Authenticated Profiles — save the post-OTP session to avoid repeating the flow
- Log In with BQL and Browser Automation — automate login flows with dynamic selector detection
- Fill and Submit a Form — general form automation patterns