| title | Programmatic Flow |
|---|---|
| description | Build your own credential collection UI with full control |
Build your own credential collection UI instead of using the hosted page. Poll for login fields, then submit credentials via the API.
Use the Programmatic flow when:
- You need a custom credential collection UI that matches your app's design
- You're building headless/automated authentication
- You have credentials stored and want to authenticate without user interaction
A Managed Auth Connection attaches an authenticated domain to a profile so you can use the auth connection in future browsers. A single profile can hold multiple auth connections — create one connection for each domain you want to keep authenticated on that profile.
```typescript TypeScript const auth = await kernel.auth.connections.create({ domain: 'github.com', profile_name: 'github-profile', // Name of the profile to associate with the connection }); ```auth = await kernel.auth.connections.create(
domain="github.com",
profile_name="github-profile", # Name of the profile to associate with the connection
)login = await kernel.auth.connections.login(auth.id)Credentials are saved automatically on successful login, enabling automatic re-authentication when the session expires.
A single loop handles everything—initial login, 2FA, and completion:
```typescript TypeScript let state = await kernel.auth.connections.retrieve(auth.id);while (state.flow_status === 'IN_PROGRESS') { // Submit when fields are ready (login or 2FA) if (state.flow_step === 'AWAITING_INPUT' && state.discovered_fields?.length) { const fieldValues = getCredentialsForFields(state.discovered_fields); await kernel.auth.connections.submit(auth.id, { fields: fieldValues }); }
await new Promise(r => setTimeout(r, 2000)); state = await kernel.auth.connections.retrieve(auth.id); }
if (state.status === 'AUTHENTICATED') { console.log('Authentication successful!'); }
```python Python
state = await kernel.auth.connections.retrieve(auth.id)
while state.flow_status == "IN_PROGRESS":
# Submit when fields are ready (login or 2FA)
if state.flow_step == "AWAITING_INPUT" and state.discovered_fields:
field_values = get_credentials_for_fields(state.discovered_fields)
await kernel.auth.connections.submit(auth.id, fields=field_values)
await asyncio.sleep(2)
state = await kernel.auth.connections.retrieve(auth.id)
if state.status == "AUTHENTICATED":
print("Authentication successful!")
The discovered_fields array tells you what the login form needs:
// Example discovered_fields for login
[{ name: 'username', type: 'text' }, { name: 'password', type: 'password' }]
// Example discovered_fields for 2FA
[{ name: 'otp', type: 'code' }]const kernel = new Kernel();
// Create connection const auth = await kernel.auth.connections.create({ domain: 'github.com', profile_name: 'github-profile', });
const login = await kernel.auth.connections.login(auth.id);
// Single polling loop handles login + 2FA let state = await kernel.auth.connections.retrieve(auth.id);
while (state.flow_status === 'IN_PROGRESS') { if (state.flow_step === 'AWAITING_INPUT' && state.discovered_fields?.length) { // Check what fields are needed const fieldNames = state.discovered_fields.map(f => f.name);
if (fieldNames.includes('username')) {
// Initial login
await kernel.auth.connections.submit(auth.id, {
fields: { username: 'my-username', password: 'my-password' }
});
} else {
// 2FA or additional fields
const code = await promptUserForCode();
await kernel.auth.connections.submit(auth.id, {
fields: { [state.discovered_fields[0].name]: code }
});
}
}
await new Promise(r => setTimeout(r, 2000)); state = await kernel.auth.connections.retrieve(auth.id); }
if (state.status === 'AUTHENTICATED') { console.log('Authentication successful!');
const browser = await kernel.browsers.create({ profile: { name: 'github-profile' }, stealth: true, });
// Navigate to the site—you're already logged in await page.goto('https://github.com'); }
```python Python
from kernel import Kernel
import asyncio
kernel = Kernel()
# Create connection
auth = await kernel.auth.connections.create(
domain="github.com",
profile_name="github-profile",
)
login = await kernel.auth.connections.login(auth.id)
# Single polling loop handles login + 2FA
state = await kernel.auth.connections.retrieve(auth.id)
while state.flow_status == "IN_PROGRESS":
if state.flow_step == "AWAITING_INPUT" and state.discovered_fields:
# Check what fields are needed
field_names = [f["name"] for f in state.discovered_fields]
if "username" in field_names:
# Initial login
await kernel.auth.connections.submit(
auth.id,
fields={"username": "my-username", "password": "my-password"},
)
else:
# 2FA or additional fields
code = input("Enter code: ")
await kernel.auth.connections.submit(
auth.id,
fields={state.discovered_fields[0]["name"]: code},
)
await asyncio.sleep(2)
state = await kernel.auth.connections.retrieve(auth.id)
if state.status == "AUTHENTICATED":
print("Authentication successful!")
browser = await kernel.browsers.create(
profile={"name": "github-profile"},
stealth=True,
)
# Navigate to the site—you're already logged in
await page.goto("https://github.com")
The basic polling loop handles discovered_fields, but login pages can require other input types too.
When the login page has "Sign in with Google/GitHub/Microsoft" buttons, they appear in pending_sso_buttons:
// Submit the selected SSO button await kernel.auth.connections.submit(auth.id, { sso_button_selector: state.pending_sso_buttons[0].selector }); }
```python Python
if state.pending_sso_buttons:
# Show the user available SSO options
for btn in state.pending_sso_buttons:
print(f"{btn['provider']}: {btn['label']}")
# Submit the selected SSO button
await kernel.auth.connections.submit(
auth.id,
sso_button_selector=state.pending_sso_buttons[0]["selector"],
)
As an alternative to clicking an SSO button by selector, you can submit the SSO provider name directly. When SSO buttons are detected, the session state includes a sso_provider field (a string) identifying the provider that Kernel recommends. You can also specify a provider explicitly using the sso_provider submit parameter:
if state.pending_sso_buttons:
# Submit by provider name instead of selector
await kernel.auth.connections.submit(
auth.id,
sso_provider=state.pending_sso_buttons[0]["provider"], # e.g., "google"
)When the site offers multiple MFA methods, they appear in mfa_options:
// Submit the selected MFA method await kernel.auth.connections.submit(auth.id, { mfa_option_id: 'sms' }); }
```python Python
if state.mfa_options:
# Available types: sms, email, totp, push, call, password, switch
for opt in state.mfa_options:
print(f"{opt['type']}: {opt['label']}")
# Submit the selected MFA method
await kernel.auth.connections.submit(
auth.id,
mfa_option_id="sms",
)
After selecting an MFA method, the flow continues. Poll for discovered_fields to submit the code, or handle external actions for push/security key.
Some sites present non-MFA choices during login, such as account selection or organization pickers. These appear in sign_in_options as an array of objects with id, label, and optional description:
// Submit the selected option await kernel.auth.connections.submit(auth.id, { sign_in_option_id: state.sign_in_options[0].id }); }
```python Python
if state.sign_in_options:
# Show available options to the user
for opt in state.sign_in_options:
print(f"{opt['id']}: {opt['label']}")
if opt.get("description"):
print(f" {opt['description']}")
# Submit the selected option
await kernel.auth.connections.submit(
auth.id,
sign_in_option_id=state.sign_in_options[0]["id"],
)
When the site requires an action outside the browser (push notification, security key tap), the step becomes AWAITING_EXTERNAL_ACTION:
// Some sites offer fallback methods alongside the external action // (e.g. "Try another way"). Submit one to switch verification methods. if (state.mfa_options?.length) { await kernel.auth.connections.submit(auth.id, { mfa_option_id: state.mfa_options[0].type, }); }
// Otherwise keep polling—the flow resumes automatically when the user completes the action }
```python Python
if state.flow_step == "AWAITING_EXTERNAL_ACTION":
# Show the message to the user
print(state.external_action_message)
# e.g., "Check your phone for a push notification"
# Some sites offer fallback methods alongside the external action
# (e.g. "Try another way"). Submit one to switch verification methods.
if state.mfa_options:
await kernel.auth.connections.submit(
auth.id,
mfa_option_id=state.mfa_options[0]["type"],
)
# Otherwise keep polling—the flow resumes automatically when the user completes the action
The flow_step field indicates what the flow is waiting for:
| Step | Description |
|---|---|
DISCOVERING |
Finding the login page and analyzing it |
AWAITING_INPUT |
Waiting for field values, SSO button click, SSO provider selection, MFA selection, or sign-in option selection |
SUBMITTING |
Processing submitted values |
AWAITING_EXTERNAL_ACTION |
Waiting for push approval, security key, etc. |
COMPLETED |
Flow has finished |
The flow_status field indicates the current flow state:
| Status | Description |
|---|---|
IN_PROGRESS |
Authentication is ongoing—keep polling |
SUCCESS |
Login completed, profile saved |
FAILED |
Login failed (check error_message) |
EXPIRED |
Flow timed out (10 minutes for user input, 20 minutes overall) |
CANCELED |
Flow was canceled |
The status field indicates the overall connection state:
| Status | Description |
|---|---|
AUTHENTICATED |
Profile is logged in and ready to use |
NEEDS_AUTH |
Profile needs authentication |
After creating a connection, you can update its configuration with PATCH /auth/connections/{id}:
| Field | Description |
|---|---|
login_url |
Override the login page URL |
credential |
Update the linked credential |
allowed_domains |
Update allowed redirect domains |
health_check_interval |
Seconds between health checks (minimum varies by plan) |
save_credentials |
Whether to save credentials on successful login |
record_session |
Record a replay of every auth browser session for this connection (logins, health checks, and reauths) |
proxy |
Proxy configuration for login sessions |
Only the fields you include are updated—everything else stays the same.
```typescript TypeScript await kernel.auth.connections.update(auth.id, { login_url: 'https://example.com/login', health_check_interval: 1800, save_credentials: true, }); ```await kernel.auth.connections.update(
auth.id,
login_url="https://example.com/login",
health_check_interval=1800,
save_credentials=True,
)For real-time UIs, you can stream login flow events via Server-Sent Events instead of polling:
GET /auth/connections/{id}/events
The stream delivers managed_auth_state events with the same fields as polling (flow_status, flow_step, discovered_fields, etc.) and terminates automatically when the flow reaches a terminal state.