> ## Documentation Index
> Fetch the complete documentation index at: https://docs.corbado.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Subflows with Corbado Observe

> Track concrete authentication methods with subflows.

Subflows are the concrete building blocks inside a [flow](/corbado-observe/tracking/flows) and are **auto-discovered** by **Corbado Observe** based on the events you send. They represent a specific authentication method, for example passkey login, password enrollment, or email OTP verification.

<Frame>
  <iframe className="w-full aspect-video rounded-xl" src="https://www.youtube.com/embed/ImkgPLTKKcQ" title="Track Passkey Enrollment in Real-Time | Corbado Observe" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen />
</Frame>

<Info>
  Install and set up the **Corbado Observe SDK**, including your first tracked event, in [Getting started](/corbado-observe/overview/getting-started). This page explains the subflow concept and documents all available subflow types.
</Info>

On SDK level, every subflow type has a dedicated class that helps with tracking. Each class defines the default steps you typically want to track for that subflow. If you need to track additional steps beyond the defaults, you can do so by using the generic step events with a custom `stepName`.

## How subflows work

A subflow groups related events for one authentication method. Every subflow has a **type** (e.g., `passkey-login` or `password-login`) and consists of a **trigger** and one or more **steps**.

### Trigger

The **trigger** (`subflow_trigger`) marks the moment of interaction with the subflow. Usually the actor is the user (e.g., clicking "Sign in with passkey"), but in some cases the system can be the actor too (e.g., during passkey enrollment with conditional mediation).

Note that steps can run **before** the interaction happens. For example, during passkey login the `get-options` step may complete in the background while the UI renders a "Sign in with passkey" button. The interaction happens only when the user clicks that button. If the user never clicks, there was no interaction with this subflow.

Corbado Observe uses the interaction moment to calculate **how long it takes the user to interact** with a subflow and to determine if a subflow **should be included in certain dashboards or not**.

**Not every subflow needs an explicit trigger event.** Check the documentation of each subflow type below: some detect the interaction on their own (e.g., provide-identifier from typing into the bound input field, passkey-login from the started `ceremony` step), while others still require you to send `subflow_trigger` (e.g., passkey enrollment). For subflow types with automatic detection, explicit triggers are deprecated.

Where an explicit trigger is required, it carries:

| Field              | Description                                                                                                                                    |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `actor`            | Who initiated the interaction: `user` (e.g., clicked a button) or `system` (e.g., auto-started passkey enrollment with conditional mediation). |
| `explicitSpecType` | Optional. A variant label that distinguishes different flavors of the same subflow type (see the spec type tables for each subflow below).     |

### Steps

Each step represents one phase of the subflow. A step is identified by its `stepName` (e.g., `get-options`, `ceremony`, `post-response`) and tracked with three generic events:

| Event                   | Description                      |
| ----------------------- | -------------------------------- |
| `subflow_step_started`  | The step began.                  |
| `subflow_step_finished` | The step completed successfully. |
| `subflow_step_error`    | The step failed.                 |

Every subflow type defines a set of predefined steps (documented in the sections below). You can also define **custom steps** by passing any `stepName` string to the generic step events.

### Example: passkey login

A successful passkey login subflow where assertion options are pre-fetched in the background:

| # | Event                   | Step            | Description                                                                                     |
| - | ----------------------- | --------------- | ----------------------------------------------------------------------------------------------- |
| 1 | `subflow_step_started`  | `get-options`   | SDK requests assertion options from your server in the background.                              |
| 2 | `subflow_step_finished` | `get-options`   | Server returns assertion options. UI can now show a "Sign in with passkey" button.              |
| 3 | `subflow_step_started`  | `ceremony`      | User clicks the button — the browser WebAuthn ceremony begins. This step marks the interaction. |
| 4 | `subflow_step_finished` | `ceremony`      | User completes the ceremony.                                                                    |
| 5 | `subflow_step_started`  | `post-response` | SDK sends the assertion response to your server for verification.                               |
| 6 | `subflow_step_finished` | `post-response` | Server confirms the login.                                                                      |

The `get-options` steps run in the background without any user involvement — only the started `ceremony` step counts as the interaction. If the user never clicks the button, the ceremony never starts and there was no interaction with this subflow.

If any step fails, a `subflow_step_error` replaces the corresponding `subflow_step_finished`.

***

## Subflow types

### 1. Provide identifier

**SDK class:** `provideIdentifierOperationFull(inputHtmlField)`

Tracks when users enter and submit their identifier (e.g., an email address).

Because passkey Conditional UI (CUI) is always bound to the same input field as the identifier, **CUI passkey login events are tracked together with the provide-identifier subflow**. This means a single integration point covers both the identifier input and an optional CUI-initiated passkey login.

#### Steps

| Step            | Subflow type         | Description                                                                              |
| --------------- | -------------------- | ---------------------------------------------------------------------------------------- |
| `post-response` | `provide-identifier` | Server-side identifier check (e.g., does user exist, which login methods are available). |
| `get-options`   | `passkey-login`      | CUI requests assertion options from the server.                                          |
| `ceremony`      | `passkey-login`      | Browser CUI ceremony (auto-fill prompt).                                                 |
| `post-response` | `passkey-login`      | Server verifies the CUI assertion response.                                              |

#### Spec types

| `explicitSpecType` | Subflow type         | Description                                                                          |
| ------------------ | -------------------- | ------------------------------------------------------------------------------------ |
| `email`            | `provide-identifier` | User typed into the identifier field (auto-detected by the SDK).                     |
| `passkey-cui`      | `passkey-login`      | User interacted with the Conditional UI auto-fill prompt (auto-detected by the SDK). |

#### Code example

The operation is bound to the identifier input field. This enables the SDK to **automatically detect** user interaction: when the user starts typing, the provide-identifier trigger fires; when the user interacts with the browser's CUI autofill prompt, the CUI trigger and ceremony start fire automatically. The SDK also auto-detects CUI cancellation.

<Note>
  You do not need to send `subflow_trigger` events for this subflow — interaction is handled automatically. Explicit triggers for provide-identifier are deprecated and will be removed in a future SDK version.
</Note>

<Tabs>
  <Tab title="Provide identifier + CUI">
    ```typescript theme={null}
    import { getTracker } from "@corbado/observe";

    const identifierInput = document.getElementById("email") as HTMLInputElement;
    const op = getTracker().provideIdentifierOperationFull(identifierInput);

    // --- CUI passkey login (runs in background on page load) ---

    const startConditionalUI = async () => {
      try {
        op.cui.getOptions.start({ explicitSpecType: "passkey-cui" });
        const options = await fetchConditionalUIOptions();
        op.cui.getOptions.finished({ assertionOptions: JSON.stringify(options) });
      } catch (e) {
        op.cui.getOptions.error(e);
        return;
      }

      try {
        // cui.trigger() and cui.ceremony.start() are auto-detected by the SDK
        const response = await startWebAuthnAuthentication(options, { useBrowserAutofill: true });
        op.cui.ceremony.finished({ assertionResponse: JSON.stringify(response) });

        op.cui.postResponse.start({});
        await verifyOnServer(response);
        op.cui.postResponse.finished({});
      } catch (e) {
        op.cui.ceremony.error(e);
      }
    };

    // --- Provide identifier (user submits the form) ---

    const handleIdentifierSubmit = async (email: string) => {
      op.provideIdentifier.postResponse.start({ explicitSpecType: "email" });

      try {
        await checkUserOnServer(email);
        op.provideIdentifier.postResponse.finished({});
      } catch (e) {
        op.provideIdentifier.postResponse.error(e);
      }
    };

    // Clean up when the component unmounts
    op.destroy();
    ```
  </Tab>
</Tabs>

***

### 2. Passkey login

**SDK class:** `passkeyLoginFullOperation()`

Tracks passkey-based sign-in attempts where the WebAuthn ceremony is started explicitly — by the relying party or via immediate mediation (not via Conditional UI — see [Provide identifier](#1-provide-identifier) for CUI).

#### Steps

| Step            | Description                                |
| --------------- | ------------------------------------------ |
| `get-options`   | Request assertion options from the server. |
| `ceremony`      | Browser WebAuthn authentication ceremony.  |
| `post-response` | Server verifies the assertion response.    |

#### Spec types

| `explicitSpecType`         | Description                                                                                                                                                                                                               |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `passkey-known-identifier` | User's identifier is already known (e.g., post-identifier screen). The server can scope the allowed credentials to this user.                                                                                             |
| `passkey-no-identifier`    | No identifier is known. The server returns a broad set of allowed credentials.                                                                                                                                            |
| `passkey-cui`              | Conditional UI initiated passkey login (typically tracked via the provide-identifier integration).                                                                                                                        |
| `passkey-immediate`        | Immediate-mediation login (`navigator.credentials.get({ mediation: "immediate" })`): the browser offers a passkey prompt right when the login page loads, before the user enters an identifier and before Conditional UI. |

<Note>
  You do not need to send `subflow_trigger` events for this subflow — the started `ceremony` step marks the interaction. Explicit triggers for passkey-login are deprecated and will be removed in a future SDK version.
</Note>

#### Code example

<Tabs>
  <Tab title="Immediate mediation (pre-identifier)">
    The browser offers a passkey prompt immediately when the login page loads — before the user has entered an identifier and before Conditional UI starts. **No trigger is sent**: there is no user click and the prompt is owned by the browser, so the started `ceremony` step marks the interaction.

    ```typescript theme={null}
    import { getTracker } from "@corbado/observe";

    const attemptImmediatePasskeyLogin = async () => {
      const passkeyOp = getTracker().passkeyLoginFullOperation();

      // Assertion options are typically embedded in the page or pre-fetched
      // before any UI is shown, so no get-options step is tracked here.
      const options = await fetchAssertionOptions();

      let response;
      try {
        passkeyOp.ceremony.start({
          explicitSpecType: "passkey-immediate",
          assertionOptions: JSON.stringify(options),
        });
        response = await startWebAuthnAuthentication(options, { mediation: "immediate" });
        passkeyOp.ceremony.finished({ assertionResponse: JSON.stringify(response) });
      } catch (e) {
        passkeyOp.ceremony.error(e);

        // Continue with the regular login: redirect the user to your login
        // component, or — if you are already on the login page — start
        // Conditional UI on the identifier field (see Provide identifier).
        return;
      }

      try {
        passkeyOp.postResponse.start({});
        await verifyOnServer(response);
        passkeyOp.postResponse.finished({});
      } catch (e) {
        passkeyOp.postResponse.error(e);
      }
    };
    ```
  </Tab>

  <Tab title="Button click (post-identifier)">
    The user's identifier is already known. A "Sign in with passkey" button is shown, and the user clicks it to start.

    ```typescript theme={null}
    import { getTracker } from "@corbado/observe";

    const handlePasskeyLogin = async (email: string) => {
      const passkeyOp = getTracker().passkeyLoginFullOperation();

      try {
        passkeyOp.getOptions.start({ explicitSpecType: "passkey-known-identifier" });
        const options = await fetchAssertionOptions(email);
        passkeyOp.getOptions.finished({ assertionOptions: JSON.stringify(options) });
      } catch (e) {
        passkeyOp.getOptions.error(e);
        return;
      }

      try {
        passkeyOp.ceremony.start({});
        const response = await startWebAuthnAuthentication(options);
        passkeyOp.ceremony.finished({ assertionResponse: JSON.stringify(response) });
      } catch (e) {
        passkeyOp.ceremony.error(e);
        return;
      }

      try {
        passkeyOp.postResponse.start({});
        await verifyOnServer(response);
        passkeyOp.postResponse.finished({});
      } catch (e) {
        passkeyOp.postResponse.error(e);
      }
    };
    ```
  </Tab>
</Tabs>

***

### 3. Passkey enrollment

**SDK class:** `passkeyEnrollmentFullOperation()`

Tracks passkey registration and setup after authentication (e.g., during a post-login enrollment prompt).

#### Steps

| Step            | Description                                                         |
| --------------- | ------------------------------------------------------------------- |
| `get-options`   | Request attestation options from the server.                        |
| `ceremony`      | Browser WebAuthn registration ceremony.                             |
| `post-response` | Server verifies the attestation response and stores the credential. |

#### Spec types

| `explicitSpecType`        | Description                                                                                                                                                        |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `conditional-auto-manual` | System first attempts conditional (auto-register) enrollment, then falls back to a regular prompt if it fails, and finally allows the user to trigger it manually. |
| `auto-manual`             | System auto-triggers the enrollment prompt, with a manual fallback.                                                                                                |
| `manual`                  | User explicitly triggers enrollment (e.g., clicks "Create passkey").                                                                                               |

#### Code example

<Tabs>
  <Tab title="Conditional mediation + auto">
    The system first attempts conditional mediation (auto-register) enrollment. If the user dismisses the conditional prompt, it falls back to a regular auto-triggered prompt.

    ```typescript theme={null}
    import { getTracker } from "@corbado/observe";

    const enrollOp = getTracker().passkeyEnrollmentFullOperation();

    const handleEnrollment = async (userId: string, isConditional: boolean, isAuto: boolean) => {
      const specType = isConditional ? "conditional-auto-manual" : "auto-manual";
      enrollOp.trigger({ actor: isConditional || isAuto ? "system" : "user" });

      try {
        enrollOp.getOptions.start({ explicitSpecType: specType });
        const options = await fetchAttestationOptions(userId);
        enrollOp.getOptions.finished({ attestationOptions: JSON.stringify(options) });
      } catch (e) {
        enrollOp.getOptions.error(e);
        return;
      }

      try {
        enrollOp.ceremony.start({ mediation: isConditional ? "conditional" : "required" });
        const response = await startWebAuthnRegistration(options, { useAutoRegister: isConditional });
        enrollOp.ceremony.finished({ attestationResponse: JSON.stringify(response) });
      } catch (e) {
        enrollOp.ceremony.error(e);
        if (isConditional) {
          // Conditional prompt dismissed — fall back to auto
          await handleEnrollment(userId, false, true);
        }
        return;
      }

      try {
        enrollOp.postResponse.start({});
        await verifyOnServer(response);
        enrollOp.postResponse.finished({});
      } catch (e) {
        enrollOp.postResponse.error(e);
      }
    };

    // Start with conditional enrollment
    await handleEnrollment(userId, true, true);
    ```
  </Tab>

  <Tab title="Auto">
    The system auto-triggers the enrollment prompt without attempting conditional mediation first.

    ```typescript theme={null}
    import { getTracker } from "@corbado/observe";

    const handleEnrollment = async (userId: string, isAuto: boolean) => {
      const enrollOp = getTracker().passkeyEnrollmentFullOperation();
      enrollOp.trigger({ actor: isAuto ? "system" : "user" });

      try {
        enrollOp.getOptions.start({ explicitSpecType: "auto-manual" });
        const options = await fetchAttestationOptions(userId);
        enrollOp.getOptions.finished({ attestationOptions: JSON.stringify(options) });
      } catch (e) {
        enrollOp.getOptions.error(e);
        return;
      }

      try {
        enrollOp.ceremony.start({ mediation: "required" });
        const response = await startWebAuthnRegistration(options);
        enrollOp.ceremony.finished({ attestationResponse: JSON.stringify(response) });
      } catch (e) {
        enrollOp.ceremony.error(e);
        return;
      }

      try {
        enrollOp.postResponse.start({});
        await verifyOnServer(response);
        enrollOp.postResponse.finished({});
      } catch (e) {
        enrollOp.postResponse.error(e);
      }
    };
    ```
  </Tab>

  <Tab title="Manual">
    The user explicitly clicks a "Create passkey" button to start enrollment.

    ```typescript theme={null}
    import { getTracker } from "@corbado/observe";

    const handleManualEnrollment = async (userId: string) => {
      const enrollOp = getTracker().passkeyEnrollmentFullOperation();
      enrollOp.trigger({ actor: "user" });

      try {
        enrollOp.getOptions.start({ explicitSpecType: "manual" });
        const options = await fetchAttestationOptions(userId);
        enrollOp.getOptions.finished({ attestationOptions: JSON.stringify(options) });
      } catch (e) {
        enrollOp.getOptions.error(e);
        return;
      }

      try {
        enrollOp.ceremony.start({ mediation: "required" });
        const response = await startWebAuthnRegistration(options);
        enrollOp.ceremony.finished({ attestationResponse: JSON.stringify(response) });
      } catch (e) {
        enrollOp.ceremony.error(e);
        return;
      }

      try {
        enrollOp.postResponse.start({});
        await verifyOnServer(response);
        enrollOp.postResponse.finished({});
      } catch (e) {
        enrollOp.postResponse.error(e);
      }
    };
    ```
  </Tab>
</Tabs>

***

### 4. Password login

**SDK class:** `passwordLoginFullOperation(autoTrackConfig?)`

Tracks password-based login attempts. The operation is bound to the password input field via `autoTrackConfig`, so the SDK **automatically detects** the interaction when the user starts typing.

<Note>
  You do not need to send `subflow_trigger` events for this subflow — interaction is handled automatically. Explicit triggers for password-login are deprecated and will be removed in a future SDK version.
</Note>

#### Steps

| Step            | Description                             |
| --------------- | --------------------------------------- |
| `post-response` | Server verifies the submitted password. |

#### Spec types

| `explicitSpecType`          | Description                                                                                                 |
| --------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `password-known-identifier` | Identifier was already submitted in a previous step (e.g., post-identifier screen with a pre-filled email). |
| `password-with-identifier`  | Identifier and password are submitted together on the same screen.                                          |

#### Typed errors

In addition to passing raw errors, the password login subflow supports typed error codes:

| Code               | Description                              |
| ------------------ | ---------------------------------------- |
| `invalid_password` | The submitted password is incorrect.     |
| `user_not_found`   | No user exists for the given identifier. |
| `account_locked`   | The account has been locked.             |

#### Code example

The operation is bound to the password input field. When the user starts typing, the SDK detects the interaction automatically — your code only tracks the server-side verification step.

<Tabs>
  <Tab title="Identifier + password (pre-identifier)">
    Identifier and password are entered together on the same screen (no separate identifier step before).

    ```typescript theme={null}
    import { getTracker, PasswordLoginOperationFull } from "@corbado/observe";

    let passwordOp: PasswordLoginOperationFull;

    // Run when the login component mounts; returns the cleanup for unmount.
    const handleOnLoad = () => {
      const passwordInput = document.getElementById("password") as HTMLInputElement;
      passwordOp = getTracker().passwordLoginFullOperation({
        explicitSpecType: "password-with-identifier",
        inputHtmlField: passwordInput,
      });

      return () => passwordOp.destroy();
    };

    const handleLoginSubmit = async (email: string, password: string) => {
      passwordOp.postResponse.start({});

      try {
        await verifyCredentialsOnServer(email, password);
        passwordOp.postResponse.finished({});
      } catch (e) {
        passwordOp.postResponse.error(e);
      }
    };
    ```
  </Tab>

  <Tab title="Password login (post-identifier)">
    The identifier was already submitted in a previous step; only the password is entered on this screen.

    ```typescript theme={null}
    import { getTracker, PasswordLoginOperationFull } from "@corbado/observe";

    let passwordOp: PasswordLoginOperationFull;

    // Run when the login component mounts; returns the cleanup for unmount.
    const handleOnLoad = () => {
      const passwordInput = document.getElementById("password") as HTMLInputElement;
      passwordOp = getTracker().passwordLoginFullOperation({
        explicitSpecType: "password-known-identifier",
        inputHtmlField: passwordInput,
      });

      return () => passwordOp.destroy();
    };

    const handlePasswordSubmit = async (email: string, password: string) => {
      passwordOp.postResponse.start({});

      try {
        await verifyPasswordOnServer(email, password);
        passwordOp.postResponse.finished({});
      } catch (e) {
        passwordOp.postResponse.error(e);
      }
    };
    ```
  </Tab>
</Tabs>

***

### 5. Password enrollment

**SDK class:** `passwordEnrollmentFullOperation(autoTrackConfig?)`

Tracks password creation or password reset during authentication journeys.

The trigger is **auto-detected** by the SDK when the user starts typing into the password input field.

#### Steps

| Step            | Description                                   |
| --------------- | --------------------------------------------- |
| `post-response` | Server processes the new or updated password. |

#### Spec types

| `explicitSpecType` | Description                                                    |
| ------------------ | -------------------------------------------------------------- |
| `password-set`     | User sets a password for the first time (e.g., during signup). |
| `password-reset`   | User resets an existing password (e.g., via a recovery flow).  |

#### Typed errors

| Code                         | Description                                               |
| ---------------------------- | --------------------------------------------------------- |
| `requirements_not_fulfilled` | The password does not meet the required complexity rules. |

***

### 6. Email OTP

**SDK class:** `emailOtpOperationFull()`

Tracks one-time-password verification sent by email.

#### Steps

| Step            | Description                                      |
| --------------- | ------------------------------------------------ |
| `send`          | Server sends the OTP email to the user.          |
| `post-response` | Server verifies the OTP code the user submitted. |
| `resend`        | User requests a new OTP code.                    |

#### Spec types

| `explicitSpecType`     | Description                                       |
| ---------------------- | ------------------------------------------------- |
| `email-otp-login`      | OTP is used as a login method.                    |
| `email-otp-enrollment` | OTP is used for enrollment or account activation. |

***

### 7. Email link

**SDK class:** `emailLinkOperationFull()`

Tracks authentication flows where users sign in or verify their identity through a link sent by email.

#### Steps

| Step            | Description                                       |
| --------------- | ------------------------------------------------- |
| `send`          | Server sends the email containing the magic link. |
| `post-response` | Server verifies the token from the clicked link.  |
| `resend`        | User requests a new email link.                   |

#### Spec types

| `explicitSpecType`      | Description                                              |
| ----------------------- | -------------------------------------------------------- |
| `email-link-login`      | Email link is used as a login or recovery method.        |
| `email-link-enrollment` | Email link is used for enrollment or account activation. |

#### Cross-environment transactions

Email links often continue in a different browser tab, browser profile, device, or app than the one where the link was requested. For example, a user can start password recovery on desktop but open the email link on mobile.

Use `crossEnvironmentTransactionID` to connect the `send` step with the later `post-response` step. The value can be chosen as needed: you can generate a new ID for the email-link transaction or reuse an existing transaction ID from your system. The only requirement is that the same value is available when sending the email and when verifying the link after it has been clicked.

`crossEnvironmentTransactionID` does not replace `userId` or `identifier`. Use it to correlate the transaction across environments, and still provide `userId` via a user reference as soon as it is known.

```typescript theme={null}
import { getTracker } from "@corbado/observe";

const tracker = () => getTracker()!;

async function sendRecoveryLink(email: string) {
  const crossEnvironmentTransactionID = crypto.randomUUID();
  const emailLink = tracker().emailLinkOperationFull();

  emailLink.send.start(
    { explicitSpecType: "email-link-login" },
    { userReference: { crossEnvironmentTransactionID } },
  );

  await sendPasswordResetEmail(email, {
    callbackState: { crossEnvironmentTransactionID },
  });

  emailLink.send.finished({});

  return crossEnvironmentTransactionID;
}

async function verifyRecoveryLink(
  token: string,
  crossEnvironmentTransactionID: string,
) {
  const emailLink = tracker().emailLinkOperationFull();

  emailLink.postResponse.start(
    { explicitSpecType: "email-link-login" },
    { userReference: { crossEnvironmentTransactionID } },
  );

  await verifyPasswordResetToken(token);

  emailLink.postResponse.finished({});
}
```

***

### 8. Social login

**SDK class:** `socialLoginOperationFull()`

Tracks authentication with social identity providers (e.g., Google, Apple, Facebook).

#### Steps

| Step               | Description                                                                                       |
| ------------------ | ------------------------------------------------------------------------------------------------- |
| `get-redirect-url` | Application initiates the OAuth redirect to the social provider.                                  |
| `exchange-code`    | Application exchanges the authorization code for tokens after the user returns from the provider. |

#### Spec types

| `explicitSpecType` | Description                                                                                             |
| ------------------ | ------------------------------------------------------------------------------------------------------- |
| `pre-identifier`   | Social login button is shown before the user enters any identifier (e.g., on the initial login screen). |
| `post-identifier`  | Social login is offered after the user has already provided an identifier.                              |

***

### 9. Coming soon

The following subflow types are planned but not yet available:

| Subflow type   | Description                                                          |
| -------------- | -------------------------------------------------------------------- |
| `sms-otp`      | OTP verification sent via SMS.                                       |
| `provide-data` | Collecting additional user data during a flow (e.g., name, address). |
| `totp`         | Time-based one-time password (authenticator app).                    |
