Skip to main content
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

1

Create Managed Auth and Start Authentication

Same as Hosted UI
2

Poll and Submit

Poll until flow_step becomes AWAITING_INPUT, then submit credentials
3

Handle 2FA

If more fields appear (2FA code), submit again—same loop handles it

Getting started

1. Create Managed Auth

const auth = await kernel.profiles.auth.create({
  domain: 'github.com',
  profile_name: 'github-profile',
});

2. Start Authentication

const login = await kernel.profiles.auth.login(auth.id);
To save credentials for automatic re-authentication:
const login = await kernel.profiles.auth.login(auth.id, {
  save_credential_as: 'my-saved-creds',
});

3. Poll and Submit Credentials

A single loop handles everything—initial login, 2FA, and completion:
let state = await kernel.profiles.auth.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.profiles.auth.submit(auth.id, { fields: fieldValues });
  }
  
  await new Promise(r => setTimeout(r, 2000));
  state = await kernel.profiles.auth.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 managed auth
const auth = await kernel.profiles.auth.create({
  domain: 'github.com',
  profile_name: 'github-profile',
});

const login = await kernel.profiles.auth.login(auth.id);

// Single polling loop handles login + 2FA
let state = await kernel.profiles.auth.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.profiles.auth.submit(auth.id, {
        fields: { username: 'my-username', password: 'my-password' }
      });
    } else {
      // 2FA or additional fields
      const code = await promptUserForCode();
      await kernel.profiles.auth.submit(auth.id, {
        fields: { [state.discovered_fields[0].name]: code }
      });
    }
  }

  await new Promise(r => setTimeout(r, 2000));
  state = await kernel.profiles.auth.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');
}

Handling Different Input Types

The basic polling loop handles discovered_fields, but login pages can require other input types too.

SSO Buttons

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.profiles.auth.submit(auth.id, {
    sso_button_selector: state.pending_sso_buttons[0].selector
  });
}
Remember to set allowed_domains on the managed auth to include the OAuth provider’s domain (e.g., accounts.google.com).

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.profiles.auth.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:
StepDescription
DISCOVERINGFinding the login page and analyzing it
AWAITING_INPUTWaiting for field values, SSO button click, or MFA selection
SUBMITTINGProcessing submitted values
AWAITING_EXTERNAL_ACTIONWaiting for push approval, security key, etc.
COMPLETEDFlow has finished

Status Reference

The flow_status field indicates the current flow state:
StatusDescription
IN_PROGRESSAuthentication is ongoing—keep polling
SUCCESSLogin completed, profile saved
FAILEDLogin failed (check error_message)
EXPIREDFlow timed out (5 minutes)
CANCELEDFlow was canceled
The status field indicates the overall managed auth state:
StatusDescription
AUTHENTICATEDProfile is logged in and ready to use
NEEDS_AUTHProfile needs authentication