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

# Factor enrollment

> Enroll and manage a customer's TOTP and passkey factors

> [!NOTE]
>
> Applies only to customers in an SCA-required region (EU). Every endpoint here
> returns **`409`** for other customers.

`SMS_OTP` needs no enrollment; a code is sent to the customer's verified phone.
`TOTP` and `PASSKEY` must be enrolled before a customer can authenticate with
them. Enrolled factors then appear in `scaChallenge.availableFactors` and can be
requested per transaction (see
[per-transaction authorization](https://agents-docs.lightspark.com/platform-overview/sca/per-transaction-authorization.md)).

Enrollment is two calls, both discriminated by a `type` field (`TOTP` or
`PASSKEY`):

* `POST /sca/factors` — start enrollment; returns the factor-specific material.
* `POST /sca/factors/confirm` — finish enrollment with the factor-specific proof.

All paths below are relative to `https://api.lightspark.com/grid/2025-10-13`.

## Enroll a TOTP authenticator

**1. Start enrollment**

```bash theme={null}
POST /sca/factors?customerId={customerId}

{ "type": "TOTP" }
```

Returns the shared secret and an `otpauth://` provisioning URI. Render `totpUri`
as a QR code (or show `secretBase32Encoded` for manual entry) so the customer can
add it to their authenticator app.

```json theme={null}
{
  "type": "TOTP",
  "secret": "…",
  "secretBase32Encoded": "ABC123…",
  "totpUri": "otpauth://totp/Grid:customer@example.com?secret=ABC123&issuer=Grid"
}
```

**2. Confirm enrollment**

Submit the `secret` from the start call plus the first code the app produces.
Grid returns one-time **recovery codes**; surface them to the customer once and
don't store them server-side.

```bash theme={null}
POST /sca/factors/confirm?customerId={customerId}

{ "type": "TOTP", "secret": "…", "code": "000000" }
```

```json theme={null}
{ "type": "TOTP", "recoveryCodes": ["ABCD-EFGH-IJKL", "MNOP-QRST-UVWX"] }
```

A wrong or expired code returns `400`. **In sandbox, the code is always
`000000`.**

## Enroll a passkey

Passkey enrollment is a standard WebAuthn registration ceremony. Grid issues the
options, the customer's device produces the credential, and you hand it back.

> [!NOTE]
>
> A customer may have **up to two passkeys**. With two enrolled, starting another
> returns `409` (`PASSKEY_ALREADY_ENROLLED`). Delete one first (see below). To
> replace a passkey, enroll the new one before deleting the old one.

**1. Start enrollment**

```bash theme={null}
POST /sca/factors?customerId={customerId}

{ "type": "PASSKEY" }
```

```json theme={null}
{
  "type": "PASSKEY",
  "options": { "…": "opaque WebAuthn PublicKeyCredentialCreationOptions" },
  "allowedOrigins": ["https://app.example.com"],
  "relyingPartyId": "app.example.com"
}
```

Pass `options` unmodified to the device's WebAuthn API
(`navigator.credentials.create`). The ceremony must run against one of
`allowedOrigins`.

**2. Confirm enrollment**

Submit the credential the device produced and the `origin` it ran against.

```bash theme={null}
POST /sca/factors/confirm?customerId={customerId}

{ "type": "PASSKEY", "origin": "https://app.example.com", "credential": { "…": "opaque WebAuthn credential" } }
```

Returns the enrolled `factor` (an `ScaFactorView`, including the `credentialId`
you'll use to delete it later). An invalid credential or origin returns `400`.

## List enrolled factors

```bash theme={null}
GET /sca/factors?customerId={customerId}
```

```json theme={null}
{
  "factors": [
    { "factor": "TOTP", "name": "Authenticator app" },
    { "factor": "PASSKEY", "credentialId": "…", "name": "iPhone" }
  ]
}
```

`credentialId` is populated only for `PASSKEY` factors.

## Delete a factor

```bash theme={null}
DELETE /sca/factors/{credentialId}?customerId={customerId}
```

Returns `204`. Use the `credentialId` from the factor list (or the confirm
response). Today only passkeys carry a `credentialId`, so this is how you remove
an enrolled passkey.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.