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
How It Works
Create Connection and Start Session
Poll and Submit
Poll until flow_step becomes AWAITING_INPUT, then submit credentials
Handle 2FA
If more fields appear (2FA code), submit again—same loop handles it
Getting started
1. Create a Connection
A Managed Auth Connection associates a profile to a domain you want to keep authenticated so you can use the auth connection future browsers. Create one for each domain + profile combination you want to keep authenticated.
const auth = await kernel.auth.connections.create({
domain: 'github.com',
profile_name: 'github-profile', // Name of the profile to associate with the connection
});
2. Start a Login Session
const login = await kernel.auth.connections.login(auth.id);
Credentials are saved automatically on successful login, enabling automatic re-authentication when the session expires.
3. Poll and Submit Credentials
A single loop handles everything—initial login, 2FA, and completion:
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!');
}
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' }]
Complete Example
import Kernel from '@onkernel/sdk';
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');
}
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:
if (state.pending_sso_buttons?.length) {
// Show the user available SSO options
for (const btn of state.pending_sso_buttons) {
console.log(`${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
});
}
Remember to set allowed_domains on the connection to include the OAuth provider’s domain (e.g., accounts.google.com).
SSO Provider Selection
When login requires selecting an SSO provider (e.g., choosing between Google, Microsoft, or GitHub), the sso_providers field lists available options. Submit the selected provider using sso_provider:
if (state.sso_providers?.length) {
// Show available SSO providers
for (const provider of state.sso_providers) {
console.log(provider);
}
// Submit the selected SSO provider
await kernel.auth.connections.submit(auth.id, {
sso_provider: 'google'
});
}
This is different from SSO buttons. SSO provider selection is used when the login flow presents a choice of identity providers, while SSO buttons are clickable elements on the login page itself.
Sign-In Options
Some login pages present non-MFA sign-in choices, such as account pickers or organization selectors. These appear in sign_in_options:
if (state.sign_in_options?.length) {
// Show available sign-in options
for (const option of state.sign_in_options) {
console.log(`${option.id}: ${option.label} - ${option.description}`);
}
// Submit the selected option
await kernel.auth.connections.submit(auth.id, {
sign_in_option_id: state.sign_in_options[0].id
});
}
Each sign-in option has an id, label, and optional description. These are distinct from MFA options—sign-in options appear during the login phase (e.g., “Which account?” or “Select your organization”), while MFA options appear after credentials are accepted.
MFA Selection
When the site offers multiple MFA methods, they appear in mfa_options:
if (state.mfa_options?.length) {
// Available types: sms, email, totp, push, call, security_key
for (const opt of state.mfa_options) {
console.log(`${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.
External Actions (Push, Security Key)
When the site requires an action outside the browser (push notification, security key tap), the step becomes AWAITING_EXTERNAL_ACTION:
if (state.flow_step === 'AWAITING_EXTERNAL_ACTION') {
// Show the message to the user
console.log(state.external_action_message);
// e.g., "Check your phone for a push notification"
// Keep polling—the flow resumes automatically when the user completes the action
}
Step Reference
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, or MFA selection |
SUBMITTING | Processing submitted values |
AWAITING_EXTERNAL_ACTION | Waiting for push approval, security key, etc. |
COMPLETED | Flow has finished |
Status Reference
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 |
Real-Time Updates with SSE
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.
Polling is recommended for most integrations. SSE is useful when building real-time UIs that need instant updates without polling delays.
Updating Connections
Update an existing connection’s configuration without recreating it using PATCH /auth/connections/{id}:
await kernel.auth.connections.update(auth.id, {
login_url: 'https://example.com/new-login',
allowed_domains: ['accounts.google.com', 'google.com'],
health_check_interval: 900, // 15 minutes, in seconds
});
Supported fields:
| Field | Description |
|---|
login_url | Override the login page URL |
credential | Link a different credential |
allowed_domains | Update SSO/OAuth allowed domains |
health_check_interval | Health check frequency in seconds (300–86400) |
save_credentials | Enable or disable credential saving |
proxy | Update the proxy configuration |
Changes take effect on the next login or health check. If a login session is currently in progress, runtime configuration changes (like allowed_domains) are applied immediately.