> ## Documentation Index
> Fetch the complete documentation index at: https://tbd-6fc993ce-cj-profile-state.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Programmatic Flow

> 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

## How It Works

<Steps>
  <Step title="Create Connection and Start Session">
    Same as [Hosted UI](/profiles/managed-auth/hosted-ui)
  </Step>

  <Step title="Poll and Submit">
    Poll until `flow_step` becomes `AWAITING_INPUT`, then submit credentials
  </Step>

  <Step title="Handle 2FA">
    If more fields appear (2FA code), submit again—same loop handles it
  </Step>
</Steps>

## Getting started

### 1. Create a Connection

<CodeGroup>
  ```typescript TypeScript theme={null}
  const auth = await kernel.auth.connections.create({
    domain: 'github.com',
    profile_name: 'github-profile',
  });
  ```

  ```python Python theme={null}
  auth = await kernel.auth.connections.create(
      domain="github.com",
      profile_name="github-profile",
  )
  ```
</CodeGroup>

### 2. Start a Login Session

<CodeGroup>
  ```typescript TypeScript theme={null}
  const login = await kernel.auth.connections.login(auth.id);
  ```

  ```python Python theme={null}
  login = await kernel.auth.connections.login(auth.id)
  ```
</CodeGroup>

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:

<CodeGroup>
  ```typescript TypeScript theme={null}
  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 theme={null}
  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!")
  ```
</CodeGroup>

The `discovered_fields` array tells you what the login form needs:

```typescript theme={null}
// Example discovered_fields for login
[{ name: 'username', type: 'text' }, { name: 'password', type: 'password' }]

// Example discovered_fields for 2FA
[{ name: 'otp', type: 'code' }]
```

## Complete Example

<CodeGroup>
  ```typescript TypeScript theme={null}
  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');
  }
  ```

  ```python Python theme={null}
  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")
  ```
</CodeGroup>

## 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`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  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
    });
  }
  ```

  ```python Python theme={null}
  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"],
      )
  ```
</CodeGroup>

<Info>
  Remember to set `allowed_domains` on the connection to include the OAuth provider's domain (e.g., `accounts.google.com`).
</Info>

### MFA Selection

When the site offers multiple MFA methods, they appear in `mfa_options`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  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'
    });
  }
  ```

  ```python Python theme={null}
  if state.mfa_options:
      # Available types: sms, email, totp, push, call, security_key
      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",
      )
  ```
</CodeGroup>

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`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  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
  }
  ```

  ```python Python theme={null}
  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"
      
      # Keep polling—the flow resumes automatically when the user completes the action
  ```
</CodeGroup>

## 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.

<Note>
  Polling is recommended for most integrations. SSE is useful when building real-time UIs that need instant updates without polling delays.
</Note>
