> ## Documentation Index
> Fetch the complete documentation index at: https://kernel.sh/docs/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. stream login events, render the canonical fields and choices, then submit the user's response with the current interaction id.

use the programmatic flow when:

* you need a custom credential collection ui
* you're building headless authentication
* you already store credentials and want to handle only the inputs KERNEL cannot resolve automatically

## How it works

<Steps>
  <Step title="Create a connection and start a session">
    Create a managed auth connection, then call `.login()`.
  </Step>

  <Step title="Stream the session state">
    Follow the connection's sse stream. When `flow_step` becomes `AWAITING_INPUT`, render `fields` and `choices` from the event.
  </Step>

  <Step title="Submit one interaction">
    Send the event's `interaction_id` with either `field_values` or `selected_choice_id`. Keep listening because the next page may produce another interaction.
  </Step>
</Steps>

## Interaction contract

Every paused interaction uses these properties together:

| Property         | Purpose                                                                                    |
| ---------------- | ------------------------------------------------------------------------------------------ |
| `interaction_id` | Opaque id for the current pause. It changes when the actionable screen changes.            |
| `fields`         | Values the user must provide.                                                              |
| `choices`        | Visible routes the user may select, including mfa, sso, account, and organization choices. |

Submit the current `interaction_id` with either `field_values` or `selected_choice_id`. Do not mix properties from different events. KERNEL rejects stale interaction ids so a delayed submission cannot act on a newer screen.

## Get started

### 1. Create a connection

A managed auth connection attaches one authenticated domain to a [profile](/docs/auth/profiles). A profile can hold multiple connections.

<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",
  )
  ```

  ```go Go theme={null}
  auth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{
  	ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{
  		Domain:      "github.com",
  		ProfileName: "github-profile",
  	},
  })
  if err != nil {
  	panic(err)
  }
  ```
</CodeGroup>

### 2. Start a login session

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

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

  ```go Go theme={null}
  _, err = client.Auth.Connections.Login(ctx, auth.ID, kernel.AuthConnectionLoginParams{})
  if err != nil {
  	panic(err)
  }
  ```
</CodeGroup>

A successful interactive login can save submitted credentials for automatic re-authentication.

### 3. Stream and submit

Listen for `AWAITING_INPUT`. Submit fields or a selected choice, then keep listening for the next interaction.

```typescript TypeScript theme={null}
const events = await kernel.auth.connections.follow(auth.id);

for await (const event of events) {
  if (
    event.event !== 'managed_auth_state' ||
    event.flow_step !== 'AWAITING_INPUT' ||
    !event.interaction_id
  ) {
    continue;
  }

  if (event.fields?.length) {
    const fieldValues: Record<string, string> = {};

    for (const field of event.fields) {
      fieldValues[field.id] = await promptUser(field);
    }

    await kernel.auth.connections.submit(auth.id, {
      interaction_id: event.interaction_id,
      field_values: fieldValues,
    });
  } else if (event.choices?.length) {
    const choice = await promptUserToChoose(event.choices);

    await kernel.auth.connections.submit(auth.id, {
      interaction_id: event.interaction_id,
      selected_choice_id: choice.id,
    });
  }
}
```

`promptUser` and `promptUserToChoose` represent your application's ui. The submission examples below show the same requests in each sdk.

<Tip>
  every programmatic login session also has a `hosted_url`. redirect the user there if you want the hosted ui to finish an unexpected state.
</Tip>

In the examples below, `state` is the current `managed_auth_state` event.

## Fields

Each field includes:

| Property | Meaning                                                                  |
| -------- | ------------------------------------------------------------------------ |
| `id`     | Stable id used as the key in `field_values`.                             |
| `ref`    | Credential meaning, such as `email`, `password`, or `sms_code`.          |
| `type`   | `identifier`, `password`, `code`, `totp_code`, `totp_secret`, or `text`. |
| `label`  | Text to show beside the input.                                           |
| `reason` | `missing` or `rejected`.                                                 |
| `hint`   | Optional context, including a masked code destination.                   |

Submit values by **field id**, not by `ref`:

<CodeGroup>
  ```typescript TypeScript theme={null}
  await kernel.auth.connections.submit(auth.id, {
    interaction_id: state.interaction_id,
    field_values: {
      [state.fields[0].id]: userValue,
    },
  });
  ```

  ```python Python theme={null}
  await kernel.auth.connections.submit(
      auth.id,
      interaction_id=state.interaction_id,
      field_values={state.fields[0].id: user_value},
  )
  ```

  ```go Go theme={null}
  _, err := client.Auth.Connections.Submit(ctx, auth.ID, kernel.AuthConnectionSubmitParams{
  	SubmitFieldsRequest: kernel.SubmitFieldsRequestParam{
  		InteractionID: kernel.String(state.InteractionID),
  		FieldValues: map[string]string{
  			state.Fields[0].ID: userValue,
  		},
  	},
  })
  if err != nil {
  	panic(err)
  }
  ```
</CodeGroup>

### Replacing a rejected credential

A field with `reason: 'rejected'` means the site explicitly refused the previous value. Prompt for a new value and submit it against the new interaction:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const rejected = state.fields?.filter(field => field.reason === 'rejected');

  if (state.interaction_id && rejected?.length) {
    const corrected = await promptUser(rejected);
    const fieldValues = Object.fromEntries(
      rejected.map(field => [field.id, corrected[field.id]])
    );

    await kernel.auth.connections.submit(auth.id, {
      interaction_id: state.interaction_id,
      field_values: fieldValues,
    });
  }
  ```

  ```python Python theme={null}
  rejected = [field for field in (state.fields or []) if field.reason == "rejected"]

  if state.interaction_id and rejected:
      corrected = await prompt_user(rejected)
      field_values = {
          field.id: corrected[field.id]
          for field in rejected
      }
      await kernel.auth.connections.submit(
          auth.id,
          interaction_id=state.interaction_id,
          field_values=field_values,
      )
  ```
</CodeGroup>

An unattended reauth run does not ask for a corrected credential. It fails with `credentials_invalid` rather than repeating a value the site already rejected.

## Choices

All selectable auth routes use the same shape. `choice.type` identifies the category:

* `mfa_method`
* `sso_provider`
* `sign_in_method`
* `auth_method`
* `identifier_method`
* `account`
* `other`

Render the visible `label`, optional `description`, and optional `masked_destination`. Submit the exact `choice.id` returned by the event:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const mfaChoices = state.choices?.filter(choice => choice.type === 'mfa_method') ?? [];
  const selected = await choose(mfaChoices);

  await kernel.auth.connections.submit(auth.id, {
    interaction_id: state.interaction_id,
    selected_choice_id: selected.id,
  });
  ```

  ```python Python theme={null}
  mfa_choices = [choice for choice in (state.choices or []) if choice.type == "mfa_method"]
  selected = await choose(mfa_choices)

  await kernel.auth.connections.submit(
      auth.id,
      interaction_id=state.interaction_id,
      selected_choice_id=selected.id,
  )
  ```

  ```go Go theme={null}
  _, err := client.Auth.Connections.Submit(ctx, auth.ID, kernel.AuthConnectionSubmitParams{
  	SubmitFieldsRequest: kernel.SubmitFieldsRequestParam{
  		InteractionID:    kernel.String(state.InteractionID),
  		SelectedChoiceID: kernel.String(selected.ID),
  	},
  })
  if err != nil {
  	panic(err)
  }
  ```
</CodeGroup>

Do not derive the submitted id from the label or mfa type. Two sms choices can have different masked destinations and different grounded targets.

### Account and organization pickers

Account and organization rows are choices with `type: 'account'` or another non-mfa choice type. Show every returned row and submit the selected stable id:

```typescript theme={null}
const accounts = state.choices?.filter(choice => choice.type === 'account');

for (const account of accounts ?? []) {
  console.log(account.label, account.description);
}
```

Stored credential values are not returned for matching. Use only the masked or display context present on each choice.

## External actions

When `flow_step` is `AWAITING_EXTERNAL_ACTION`, show `external_action_message` and keep listening. The flow resumes when the external action completes.

Some external-action screens also expose fallback `fields` or `choices`. If `interaction_id` is present, submit a fallback through the same canonical contract:

```typescript theme={null}
if (
  state.flow_step === 'AWAITING_EXTERNAL_ACTION' &&
  state.interaction_id &&
  state.choices?.length
) {
  const fallback = await choose(state.choices);
  await kernel.auth.connections.submit(auth.id, {
    interaction_id: state.interaction_id,
    selected_choice_id: fallback.id,
  });
}
```

## Step reference

| Step                       | Description                                                                    |
| -------------------------- | ------------------------------------------------------------------------------ |
| `DISCOVERING`              | Finding and inspecting the login surface.                                      |
| `AWAITING_INPUT`           | Waiting for canonical fields or choices.                                       |
| `AWAITING_EXTERNAL_ACTION` | Waiting for an out-of-browser action; canonical fallbacks may also be present. |
| `SUBMITTING`               | Processing the submitted interaction.                                          |
| `COMPLETED`                | The flow has finished.                                                         |

## Status reference

| Status        | Description                                             |
| ------------- | ------------------------------------------------------- |
| `IN_PROGRESS` | Authentication is ongoing.                              |
| `SUCCESS`     | Login completed and the profile was saved.              |
| `FAILED`      | Login failed; inspect `error_code` and `error_message`. |
| `EXPIRED`     | The flow timed out.                                     |
| `CANCELED`    | The flow was canceled or superseded.                    |

The connection's overall `status` is `AUTHENTICATED` or `NEEDS_AUTH`.

## Connection configuration

Connection-level options such as a custom login url, allowed domains, proxy, session recording, and health-check interval apply to both hosted and programmatic flows. See [connection configuration](/docs/auth/configuration).

## SSE stream behavior

`auth.connections.follow()` opens:

```
GET /auth/connections/{id}/events
```

The stream delivers `managed_auth_state` events and closes when the flow succeeds, fails, expires, or is canceled. Prefer the stream over polling so your ui receives each interaction id in order.
