Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Onboarding

Initialize the PasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClient, then run the onboarding flow that fits your platform.

Initialization

PasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClientPasskeyClient is the entry point for every passkey wallet operation. Construct one per app session and reuse it.

On web, iOS, Android, Flutter, and React Native it wires the built-in PasskeyProviderPasskeyProviderPasskeyProviderPasskeyProviderPasskeyProviderPasskeyProviderPasskeyProviderPasskeyProviderPasskeyProvider for you, defaulting to the Breez shared RP (keys.breez.technology): a Breez-registered app needs only its Breez API key. Set provider_optionsprovider_optionsproviderOptionsproviderOptionsproviderOptionsproviderOptionsproviderOptionsProviderOptionsProviderOptions on the config to use your own RP or customize the picker identity. On other platforms, or for a custom PRF backend (hardware key, file-backed), implement PrfProviderPrfProviderPrfProviderPrfProviderPrfProviderPrfProviderPrfProviderPrfProviderPrfProvider and inject it:

Rust
let prf_provider = Arc::new(CustomPrfProvider);
PasskeyClient::new(prf_provider, Some("<breez api key>".to_string()), None)
Swift
let passkey = PasskeyClient(
    breezApiKey: "<breez api key>",
    config: PasskeyConfig(
        providerOptions: PasskeyProviderOptions(rpId: "<your-rp-domain>", rpName: "Your App")
    )
)
Kotlin
val passkey = PasskeyClient(
    breezApiKey = "<breez api key>",
    activityProvider = { activity },
    config = PasskeyConfig(
        providerOptions = PasskeyProviderOptions(rpId = "<your-rp-domain>", rpName = "Your App"),
    ),
)
C#
var prfProvider = new CustomPrfProvider();
return new PasskeyClient(prfProvider, "<breez api key>", null);
Javascript
const passkey = new PasskeyClient('<breez api key>', {
  providerOptions: { rpId: '<your-rp-domain>', rpName: 'Your App' }
})
React Native
const passkey = new PasskeyClient(
  '<breez api key>',
  PasskeyConfig.create({
    providerOptions: PasskeyProviderOptions.create({ rpId: '<your-rp-domain>', rpName: 'Your App' })
  })
)
Flutter
final passkey = PasskeyClient(
  breezApiKey: '<breez api key>',
  config: PasskeyConfig(
    providerOptions: PasskeyProviderOptions(rpId: '<your-rp-domain>', rpName: 'Your App'),
  ),
);
Python
prf_provider = CustomPrfProvider()
passkey = PasskeyClient(prf_provider, "<breez api key>", None)
Go
prfProvider := &CustomPrfProvider{}
apiKey := "<breez api key>"
return breez_sdk_spark.NewPasskeyClient(prfProvider, &apiKey, nil)

Parameters:

ParameterDefaultDescription
breez_api_keybreez_api_keybreezApiKeybreezApiKeybreezApiKeybreezApiKeybreezApiKeyBreezApiKeyBreezApiKeyrequiredYour Breez API key, used to authenticate to the Breez relay for label storage.
default_labeldefault_labeldefaultLabeldefaultLabeldefaultLabeldefaultLabeldefaultLabelDefaultLabelDefaultLabel"Default"Wallet label used when PasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.RegisterPasskeyClient.Register / PasskeyClient.sign_inPasskeyClient.sign_inPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.SignInPasskeyClient.SignIn receive none. Set on passkey_configpasskey_configpasskeyConfigpasskeyConfigpasskeyConfigpasskeyConfigpasskeyConfigPasskeyConfigPasskeyConfig.

Configure the built-in provider through provider_optionsprovider_optionsproviderOptionsproviderOptionsproviderOptionsproviderOptionsproviderOptionsProviderOptionsProviderOptions on passkey_configpasskey_configpasskeyConfigpasskeyConfigpasskeyConfigpasskeyConfigpasskeyConfigPasskeyConfigPasskeyConfig (a PasskeyProviderOptionsPasskeyProviderOptionsPasskeyProviderOptionsPasskeyProviderOptionsPasskeyProviderOptionsPasskeyProviderOptionsPasskeyProviderOptionsPasskeyProviderOptionsPasskeyProviderOptions):

FieldDefaultDescription
rp_idrp_idrpIdrpIdrpIdrpIdrpIdRpIdRpIdBreez shared RPRelying Party ID: your app's domain, or unset for the Breez shared RP (keys.breez.technology) if your app is Breez-registered. Changing it later strands existing credentials.
rp_namerp_namerpNamerpNamerpNamerpNamerpNameRpNameRpName"Breez"Display name for your app, shown in some authenticator UIs.
user_nameuser_nameuserNameuserNameuserNameuserNameuserNameUserNameUserNamerp_namerp_namerpNamerpNamerpNamerpNamerpNameRpNameRpNameAccount identifier the OS sign-in picker shows beneath the display name, e.g. john@doe.com. Set a stable per-user value to keep each registration a distinct entry.
user_display_nameuser_display_nameuserDisplayNameuserDisplayNameuserDisplayNameuserDisplayNameuserDisplayNameUserDisplayNameUserDisplayNameuser_nameuser_nameuserNameuserNameuserNameuserNameuserNameUserNameUserNameHuman-friendly name the picker shows most prominently, e.g. John Doe.

For platform-specific provider options (iOS URLSession / presentation anchor, Android Activity, web authenticatorAttachment) or a custom PRF backend, build the provider yourself and inject it. See PRF providers.

Checking passkey availability

Call PasskeyClient.check_availabilityPasskeyClient.check_availabilityPasskeyClient.checkAvailabilityPasskeyClient.checkAvailabilityPasskeyClient.checkAvailabilityPasskeyClient.checkAvailabilityPasskeyClient.checkAvailabilityPasskeyClient.CheckAvailabilityPasskeyClient.CheckAvailability before showing the passkey button. One call covers device support and your domain config, so you can hide the option on unsupported devices (older Android / iOS) or surface a configuration error (missing entitlement, undeployed AASA) before the user runs into an opaque WebAuthn failure.

Rust
match passkey.check_availability().await? {
    PasskeyAvailability::Available => {
        // Show passkey as primary option.
    }
    PasskeyAvailability::PrfUnsupported => {
        // Fall back to mnemonic flow.
    }
    PasskeyAvailability::NotAssociated { source, reason } => {
        eprintln!("Domain association failed (source={source}): {reason}");
    }
    PasskeyAvailability::Skipped { reason: _ } => {
        // No verification source on this platform; proceed normally.
    }
}
Swift
switch try await passkey.checkAvailability() {
case .available:
    // Show passkey as primary option.
    break
case .prfUnsupported:
    // Fall back to mnemonic flow.
    break
case .notAssociated(let source, let reason):
    print("Domain association failed (source=\(source)): \(reason)")
case .skipped:
    // No verification source on this platform; proceed normally.
    break
}
Kotlin
when (val availability = passkey.checkAvailability()) {
    is PasskeyAvailability.Available -> Unit
    is PasskeyAvailability.PrfUnsupported -> Unit
    is PasskeyAvailability.NotAssociated -> {
        // Log.e("Breez", "Domain association failed
        // (source=${availability.source}): ${availability.reason}")
    }
    is PasskeyAvailability.Skipped -> Unit
}
C#
switch (await passkey.CheckAvailability())
{
    case PasskeyAvailability.Available:
        break;
    case PasskeyAvailability.PrfUnsupported:
        break;
    case PasskeyAvailability.NotAssociated notAssociated:
        Console.WriteLine($"Domain association failed (source={notAssociated.source}): " +
                          $"{notAssociated.reason}");
        break;
    case PasskeyAvailability.Skipped:
        break;
}
Javascript
const availability = await passkey.checkAvailability()
switch (availability.type) {
  case 'available':
    // Show passkey as primary option.
    break
  case 'prfUnsupported':
    // Fall back to mnemonic flow.
    break
  case 'notAssociated':
    console.error(
      `Domain association failed (source=${availability.source}): ${availability.reason}`
    )
    break
  case 'skipped':
    // No verification source on this platform; proceed normally.
    break
}
React Native
const availability = await passkey.checkAvailability()
switch (availability.tag) {
  case PasskeyAvailability_Tags.Available:
    // Show passkey as primary option.
    break
  case PasskeyAvailability_Tags.PrfUnsupported:
    // Fall back to mnemonic flow.
    break
  case PasskeyAvailability_Tags.NotAssociated:
    console.error(
      `Domain association failed (source=${availability.inner.source}): ` +
      `${availability.inner.reason}`
    )
    break
  case PasskeyAvailability_Tags.Skipped:
    // No verification source on this platform; proceed normally.
    break
}
Flutter
final availability = await passkey.checkAvailability();
if (availability is PasskeyAvailability_Available) {
  // Show passkey as primary option.
} else if (availability is PasskeyAvailability_PrfUnsupported) {
  // Fall back to mnemonic flow.
} else if (availability is PasskeyAvailability_NotAssociated) {
  print("Domain association failed (source=${availability.source}): ${availability.reason}");
} else if (availability is PasskeyAvailability_Skipped) {
  // No verification source on this platform; proceed normally.
}
Python
availability = await passkey.check_availability()
if isinstance(availability, PasskeyAvailability.AVAILABLE):
    # Show passkey as primary option.
    pass
elif isinstance(availability, PasskeyAvailability.PRF_UNSUPPORTED):
    # Fall back to mnemonic flow.
    pass
elif isinstance(availability, PasskeyAvailability.NOT_ASSOCIATED):
    print(f"Domain association failed (source={availability.source}): {availability.reason}")
elif isinstance(availability, PasskeyAvailability.SKIPPED):
    # No verification source on this platform; proceed normally.
    pass
Go
availability, err := passkey.CheckAvailability()
if err != nil {
	return
}
switch r := availability.(type) {
case breez_sdk_spark.PasskeyAvailabilityAvailable:
	// Show passkey as primary option.
	_ = r
case breez_sdk_spark.PasskeyAvailabilityPrfUnsupported:
	// Fall back to mnemonic flow.
	_ = r
case breez_sdk_spark.PasskeyAvailabilityNotAssociated:
	log.Printf("Domain association failed (source=%s): %s", r.Source, r.Reason)
case breez_sdk_spark.PasskeyAvailabilitySkipped:
	// No verification source on this platform; proceed normally.
	_ = r
}

Choosing a flow

The right flow depends on the platform:

  • iOS / Android use a single-call unified flow backed by PasskeyClient.connect_with_passkeyPasskeyClient.connect_with_passkeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.ConnectWithPasskeyPasskeyClient.ConnectWithPasskey.
  • Web uses two buttons ("Create a new passkey" and "Sign in with a passkey") and lets the user pick.

For explicit control over each path, call PasskeyClient.sign_inPasskeyClient.sign_inPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.SignInPasskeyClient.SignIn and PasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.RegisterPasskeyClient.Register directly.

Unified flow (iOS / Android)

One "Use Passkey" button: a silent sign-in for returning users, with automatic fall-through to registration on a fresh device.

The response's credentialcredentialcredentialcredentialcredentialcredentialcredentialCredentialCredential field carries whichever credential signed in or was registered. See Credential metadata for using it.

Rust
// Single-CTA onboarding: silent sign-in, fall through to register.
let response = passkey
    .connect_with_passkey(ConnectWithPasskeyRequest {
        label: Some("personal".to_string()),
        ..Default::default()
    })
    .await?;

let config = default_config(Network::Mainnet);
let sdk = connect(ConnectRequest {
    config,
    seed: response.wallet.seed,
    storage_dir: "./.data".to_string(),
})
.await?;
Swift
// Single-CTA onboarding: silent sign-in, fall through to register.
var config = defaultConfig(network: .mainnet)
config.apiKey = "<breez api key>"

let response = try await passkey.connectWithPasskey(
    request: ConnectWithPasskeyRequest(label: "personal")
)

let sdk = try await connect(
    request: ConnectRequest(
        config: config,
        seed: response.wallet.seed,
        storageDir: "./.data"
    ))
Kotlin
// Single-CTA onboarding: silent sign-in, fall through to register.
val config = defaultConfig(Network.MAINNET).apply { apiKey = "<breez api key>" }
val response = passkey.connectWithPasskey(
    ConnectWithPasskeyRequest(label = "personal")
)

val sdk = connect(ConnectRequest(config, response.wallet.seed, "./.data"))
C#
// Single-CTA onboarding: silent sign-in for a returning user,
// fall-through to register on a fresh device.
var response = await passkey.ConnectWithPasskey(
    new ConnectWithPasskeyRequest(label: "personal")
);

var config = BreezSdkSparkMethods.DefaultConfig(network: Network.Mainnet);
var sdk = await BreezSdkSparkMethods.Connect(new ConnectRequest(
    config: config,
    seed: response.wallet.seed,
    storageDir: "./.data"
));
Javascript
// Not available on web; use two buttons (signIn / register) instead.
const response = await passkey.signIn({ label: 'personal' })

const config = defaultConfig('mainnet')
const sdk = await connect({ config, seed: response.wallet.seed, storageDir: './.data' })
React Native
// Silent sign-in, fall through to register.
const config = { ...defaultConfig(Network.Mainnet), apiKey: '<breez api key>' }
const response = await passkey.connectWithPasskey({
  label: 'personal',
  allowCredentials: undefined,
  excludeCredentials: undefined
})

const sdk = await connect({ config, seed: response.wallet.seed, storageDir: './.data' })
Flutter
// Single-CTA onboarding: silent sign-in, fall through to register.
final config = defaultConfig(network: Network.mainnet)
    .copyWith(apiKey: '<breez api key>');
final response = await passkey.connectWithPasskey(
  request: ConnectWithPasskeyRequest(label: 'personal'),
);

final sdk = await connect(
    request: ConnectRequest(
        config: config, seed: response.wallet.seed, storageDir: "./.data"));
Python
# Silent sign-in for a returning user, fall-through to register on a fresh device.
response = await passkey.connect_with_passkey(
    ConnectWithPasskeyRequest(label="personal")
)

config = default_config(network=Network.MAINNET)
sdk = await connect(
    ConnectRequest(config=config, seed=response.wallet.seed, storage_dir="./.data")
)
Go
// Silent sign-in for a returning user, fall-through to register on a fresh device.
label := "personal"
response, err := passkey.ConnectWithPasskey(breez_sdk_spark.ConnectWithPasskeyRequest{
	Label: &label,
})
if err != nil {
	return nil, err
}

config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet)
sdk, err := breez_sdk_spark.Connect(breez_sdk_spark.ConnectRequest{
	Config:     config,
	Seed:       response.Wallet.Seed,
	StorageDir: "./.data",
})
if err != nil {
	return nil, err
}

Two-button flow (Web)

On web, present two buttons: Create a new passkey (calls PasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.RegisterPasskeyClient.Register) and Sign in with a passkey (calls PasskeyClient.sign_inPasskeyClient.sign_inPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.SignInPasskeyClient.SignIn).

PasskeyClient.connect_with_passkeyPasskeyClient.connect_with_passkeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.connectWithPasskeyPasskeyClient.ConnectWithPasskeyPasskeyClient.ConnectWithPasskey is not available on the WASM target. WebAuthn reports "no credential" and "user cancelled" identically, so the SDK can't auto-detect the flow. Let the user choose.

Sign in and register

Call PasskeyClient.sign_inPasskeyClient.sign_inPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.SignInPasskeyClient.SignIn and PasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.RegisterPasskeyClient.Register directly for explicit control: the two web buttons, separate create-a-passkey and sign-in screens, or adding a new label for a returning user. Pass wallet.seed to connectconnectconnectconnectconnectconnectconnectConnectConnect in either case.

Sign in

Sign in to an existing credential:

Rust
// Returning-user-only sign-in. No fall-through to register.
Ok(passkey
    .sign_in(SignInRequest {
        label: Some("personal".to_string()),
        ..Default::default()
    })
    .await?)
Swift
// Returning-user sign-in. No fall-through to register.
return try await passkey.signIn(request: SignInRequest(label: "personal"))
Kotlin
// Returning-user sign-in. No fall-through to register.
return passkey.signIn(SignInRequest(label = "personal"))
Javascript
// Returning-user sign-in. No fall-through to register.
return await passkey.signIn({ label: 'personal' })
React Native
// Returning-user sign-in. No fall-through to register.
return await passkey.signIn({
  label: 'personal',
  allowCredentials: undefined,
  preferImmediatelyAvailableCredentials: undefined
})
Flutter
// Returning-user sign-in. No fall-through to register.
return await passkey.signIn(request: SignInRequest(label: 'personal'));

Register

Register a fresh credential:

Rust
let response = passkey
    .register(RegisterRequest {
        label: Some("personal".to_string()),
        ..Default::default()
    })
    .await?;

let config = default_config(Network::Mainnet);
let sdk = connect(ConnectRequest {
    config,
    seed: response.wallet.seed,
    storage_dir: "./.data".to_string(),
})
.await?;
Swift
var config = defaultConfig(network: .mainnet)
config.apiKey = "<breez api key>"

let response = try await passkey.register(
    request: RegisterRequest(label: "personal")
)

let sdk = try await connect(
    request: ConnectRequest(
        config: config,
        seed: response.wallet.seed,
        storageDir: "./.data"
    ))
Kotlin
val config = defaultConfig(Network.MAINNET).apply { apiKey = "<breez api key>" }
val response = passkey.register(RegisterRequest(label = "personal"))

val sdk = connect(ConnectRequest(config, response.wallet.seed, "./.data"))
C#
var response = await passkey.Register(new RegisterRequest(label: "personal"));

var config = BreezSdkSparkMethods.DefaultConfig(network: Network.Mainnet);
var sdk = await BreezSdkSparkMethods.Connect(new ConnectRequest(
    config: config,
    seed: response.wallet.seed,
    storageDir: "./.data"
));
Javascript
const response = await passkey.register({ label: 'personal' })

const config = defaultConfig('mainnet')
const sdk = await connect({ config, seed: response.wallet.seed, storageDir: './.data' })
React Native
const config = { ...defaultConfig(Network.Mainnet), apiKey: '<breez api key>' }
const response = await passkey.register({ label: 'personal', excludeCredentials: undefined })

const sdk = await connect({ config, seed: response.wallet.seed, storageDir: './.data' })
Flutter
final config = defaultConfig(network: Network.mainnet)
    .copyWith(apiKey: '<breez api key>');
final response = await passkey.register(
  request: RegisterRequest(label: 'personal'),
);

final sdk = await connect(
    request: ConnectRequest(
        config: config, seed: response.wallet.seed, storageDir: "./.data"));
Python
response = await passkey.register(RegisterRequest(label="personal"))

config = default_config(network=Network.MAINNET)
sdk = await connect(
    ConnectRequest(config=config, seed=response.wallet.seed, storage_dir="./.data")
)
Go
label := "personal"
response, err := passkey.Register(breez_sdk_spark.RegisterRequest{Label: &label})
if err != nil {
	return nil, err
}

config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet)
sdk, err := breez_sdk_spark.Connect(breez_sdk_spark.ConnectRequest{
	Config:     config,
	Seed:       response.Wallet.Seed,
	StorageDir: "./.data",
})
if err != nil {
	return nil, err
}

Error recovery

Every passkey failure normalizes to a PrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderErrorPrfProviderError variant. Match on the variant to drive recovery:

VariantWhat it meansRecommended action
PrfProviderError::UserCancelledPrfProviderError.USER_CANCELLEDPrfProviderError.userCancelledPrfProviderError.UserCancelledPrfProviderError.UserCancelledPrfProviderError.UserCancelledPrfProviderError.UserCancelledPrfProviderErrorUserCancelledPrfProviderError.UserCancelledUser dismissed the OS promptSticky retry UI with "Try Again".
PrfProviderError::CredentialNotFoundPrfProviderError.CREDENTIAL_NOT_FOUNDPrfProviderError.credentialNotFoundPrfProviderError.CredentialNotFoundPrfProviderError.CredentialNotFoundPrfProviderError.CredentialNotFoundPrfProviderError.CredentialNotFoundPrfProviderErrorCredentialNotFoundPrfProviderError.CredentialNotFoundNo matching credential on this deviceFall through to PasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.registerPasskeyClient.RegisterPasskeyClient.Register.
PrfProviderError::CredentialAlreadyExistsPrfProviderError.CREDENTIAL_ALREADY_EXISTSPrfProviderError.credentialAlreadyExistsPrfProviderError.CredentialAlreadyExistsPrfProviderError.CredentialAlreadyExistsPrfProviderError.CredentialAlreadyExistsPrfProviderError.CredentialAlreadyExistsPrfProviderErrorCredentialAlreadyExistsPrfProviderError.CredentialAlreadyExistsRegister hit a credential in exclude_credentialsexclude_credentialsexcludeCredentialsexcludeCredentialsexcludeCredentialsexcludeCredentialsexcludeCredentialsExcludeCredentialsExcludeCredentialsFlip to PasskeyClient.sign_inPasskeyClient.sign_inPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.signInPasskeyClient.SignInPasskeyClient.SignIn; the OS picker surfaces the existing credential.
PrfProviderError::UserTimedOutPrfProviderError.USER_TIMED_OUTPrfProviderError.userTimedOutPrfProviderError.UserTimedOutPrfProviderError.UserTimedOutPrfProviderError.UserTimedOutPrfProviderError.UserTimedOutPrfProviderErrorUserTimedOutPrfProviderError.UserTimedOutOS biometric inactivity timeout, distinct from a cancelSticky retry with timeout-specific copy. Do not auto-retry.
PrfProviderError::PrfNotSupportedPrfProviderError.PRF_NOT_SUPPORTEDPrfProviderError.prfNotSupportedPrfProviderError.PrfNotSupportedPrfProviderError.PrfNotSupportedPrfProviderError.PrfNotSupportedPrfProviderError.PrfNotSupportedPrfProviderErrorPrfNotSupportedPrfProviderError.PrfNotSupportedAuthenticator lacks the PRF extensionFall back to mnemonic onboarding.
PrfProviderError::ConfigurationPrfProviderError.CONFIGURATIONPrfProviderError.configurationPrfProviderError.ConfigurationPrfProviderError.ConfigurationPrfProviderError.ConfigurationPrfProviderError.ConfigurationPrfProviderErrorConfigurationPrfProviderError.ConfigurationEntitlement missing, AASA stale, or assetlinks malformedDeveloper-facing error; surface the PasskeyAvailability::NotAssociatedPasskeyAvailability.NOT_ASSOCIATEDPasskeyAvailability.notAssociatedPasskeyAvailability.NotAssociatedPasskeyAvailability.NotAssociatedPasskeyAvailability.NotAssociatedPasskeyAvailability.NotAssociatedPasskeyAvailabilityNotAssociatedPasskeyAvailability.NotAssociated reason.
PrfProviderError::GenericPrfProviderError.GENERICPrfProviderError.genericPrfProviderError.GenericPrfProviderError.GenericPrfProviderError.GenericPrfProviderError.GenericPrfProviderErrorGenericPrfProviderError.GenericNetwork or generic failureGeneric "try again later" UI.

Web exposes typed exception classes (PasskeyAlreadyExistsError, PasskeyTimedOutError, PasskeyCredentialNotFoundError) for instanceof matching. Rust callers can branch on the collapsed error.kind() instead of every variant.

Two recovery paths are common enough to show in full.

Flip to sign-in when register hits an existing credential:

Rust
match passkey
    .register(RegisterRequest {
        label: Some("personal".to_string()),
        exclude_credentials: Some(vec![
            // app-persisted credential IDs from prior registrations
        ]),
    })
    .await
{
    Ok(response) => Ok(response.wallet),
    Err(e) if e.kind() == ErrorKind::AlreadyExists => {
        // A matching credential already exists; sign in to it instead.
        let response = passkey
            .sign_in(SignInRequest {
                label: Some("personal".to_string()),
                ..Default::default()
            })
            .await?;
        Ok(response.wallet)
    }
    Err(e) => Err(e.into()),
}
Swift
do {
    let response = try await passkey.register(
        request: RegisterRequest(
            label: "personal",
            excludeCredentials: [
                // app-persisted credential IDs from prior registrations
            ]
        )
    )
    return response.wallet
} catch PrfProviderError.CredentialAlreadyExists {
    // A matching credential already exists; sign in instead.
    let response = try await passkey.signIn(
        request: SignInRequest(label: "personal")
    )
    return response.wallet
}
Kotlin
return try {
    val response = passkey.register(
        RegisterRequest(
            label = "personal",
            // app-persisted credential IDs from prior registrations
            excludeCredentials = emptyList(),
        )
    )
    response.wallet
} catch (e: PrfProviderException.CredentialAlreadyExists) {
    // A matching credential already exists; sign in to it instead.
    val response = passkey.signIn(SignInRequest(label = "personal"))
    response.wallet
}
C#
try
{
    var response = await passkey.Register(new RegisterRequest(
        label: "personal",
        excludeCredentials: new byte[][]
        {
            // app-persisted credential IDs from prior registrations
        }
    ));
    return response.wallet;
}
catch (PrfProviderException.CredentialAlreadyExists)
{
    var response = await passkey.SignIn(new SignInRequest(label: "personal"));
    return response.wallet;
}
Javascript
try {
  const response = await passkey.register({
    label: 'personal',
    excludeCredentials: [
      // app-persisted credential IDs from prior registrations
    ]
  })
  return response.wallet
} catch (error) {
  if (error instanceof PasskeyAlreadyExistsError) {
    // A matching credential already exists; sign in to it instead.
    const response = await passkey.signIn({ label: 'personal' })
    return response.wallet
  }
  throw error
}
React Native
try {
  const response = await passkey.register({
    label: 'personal',
    excludeCredentials: [
      // app-persisted credential IDs from prior registrations
    ]
  })
  return response.wallet
} catch (error) {
  if (error instanceof PasskeyPrfException && error.code === 'credentialAlreadyExists') {
    // A matching credential already exists; sign in to it instead.
    const response = await passkey.signIn({
      label: 'personal',
      allowCredentials: undefined,
      preferImmediatelyAvailableCredentials: undefined
    })
    return response.wallet
  }
  throw error
}
Flutter
try {
  final response = await passkey.register(
    request: RegisterRequest(
      label: 'personal',
      excludeCredentials: [
        // app-persisted credential IDs from prior registrations
      ],
    ),
  );
  return response.wallet;
} on PasskeyPrfException catch (e) {
  if (e.code != 'credentialAlreadyExists') rethrow;
  // A matching credential already exists; sign in instead.
  final response = await passkey.signIn(
    request: SignInRequest(label: 'personal'),
  );
  return response.wallet;
}
Python
try:
    await passkey.register(
        RegisterRequest(
            label="personal",
            exclude_credentials=[
                # app-persisted credential IDs from prior registrations
            ],
        )
    )
except PrfProviderError.CredentialAlreadyExists:
    # A matching credential already exists; sign in to it instead.
    response = await passkey.sign_in(SignInRequest(label="personal"))
    return response.wallet
Go
label := "personal"
registerResponse, err := passkey.Register(breez_sdk_spark.RegisterRequest{
	Label:              &label,
	ExcludeCredentials: &[][]byte{
		// app-persisted credential IDs from prior registrations
	},
})
if err == nil {
	return &registerResponse.Wallet, nil
}

if !errors.Is(err, breez_sdk_spark.ErrPrfProviderErrorCredentialAlreadyExists) {
	return nil, err
}

// A matching credential already exists; sign in to it instead.
signInResponse, err := passkey.SignIn(breez_sdk_spark.SignInRequest{Label: &label})
if err != nil {
	return nil, err
}
return &signInResponse.Wallet, nil

Show a sticky retry when the biometric timeout fires:

Rust
// Biometric inactivity timeout, distinct from a user cancel.
match passkey
    .sign_in(SignInRequest {
        label: Some("personal".to_string()),
        ..Default::default()
    })
    .await
{
    Ok(response) => Ok(response),
    Err(e) if e.kind() == ErrorKind::Timeout => {
        // Show a retry UI. Do NOT auto-retry without user input.
        println!("Sign-in timed out: show \"Try Again\" UI.");
        Err(e.into())
    }
    Err(e) => Err(e.into()),
}
Swift
do {
    return try await passkey.signIn(
        request: SignInRequest(label: "personal")
    )
} catch PrfProviderError.UserTimedOut {
    // Show a retry UI. Do NOT auto-retry without user input.
    print("Sign-in timed out: show \"Try Again\" UI.")
    throw PrfProviderError.UserTimedOut
}
Kotlin
return try {
    passkey.signIn(SignInRequest(label = "personal"))
} catch (e: PrfProviderException.UserTimedOut) {
    // Show a retry UI. Do NOT auto-retry without user input.
    // Log.v("Breez", "Sign-in timed out: show \"Try Again\" UI.")
    throw e
}
C#
try
{
    return await passkey.SignIn(new SignInRequest(label: "personal"));
}
catch (PrfProviderException.UserTimedOut)
{
    Console.WriteLine("Sign-in timed out: show \"Try Again\" UI.");
    throw;
}
Javascript
// Biometric inactivity timeout, distinct from a user cancel.
try {
  const response = await passkey.signIn({ label: 'personal' })
  return response
} catch (error) {
  if (error instanceof PasskeyTimedOutError) {
    // Show a retry UI. Do NOT auto-retry without user input.
    console.log('Sign-in timed out: show "Try Again" UI.')
  }
  throw error
}
React Native
// Biometric inactivity timeout, distinct from a user cancel.
try {
  const response = await passkey.signIn({
    label: 'personal',
    allowCredentials: undefined,
    preferImmediatelyAvailableCredentials: undefined
  })
  return response
} catch (error) {
  if (error instanceof PasskeyPrfException && error.code === 'userTimedOut') {
    // Show a retry UI. Do NOT auto-retry without user input.
    console.log('Sign-in timed out: show "Try Again" UI.')
  }
  throw error
}
Flutter
// Timeout is distinct from a cancel: surface a re-prompt UI.
try {
  return await passkey.signIn(
    request: SignInRequest(label: 'personal'),
  );
} on PasskeyPrfException catch (e) {
  if (e.code == 'userTimedOut') {
    // Show a retry UI. Do NOT auto-retry without user input.
    print("Sign-in timed out: show \"Try Again\" UI.");
  }
  rethrow;
}
Python
# Biometric inactivity timeout, distinct from a user cancel.
try:
    return await passkey.sign_in(SignInRequest(label="personal"))
except PrfProviderError.UserTimedOut:
    # Show a retry UI. Do NOT auto-retry without user input.
    print("Sign-in timed out: show \"Try Again\" UI.")
    raise
Go
// Biometric inactivity timeout, distinct from a user cancel.
label := "personal"
response, err := passkey.SignIn(breez_sdk_spark.SignInRequest{Label: &label})
if err != nil {
	if errors.Is(err, breez_sdk_spark.ErrPrfProviderErrorUserTimedOut) {
		// Show a retry UI. Do NOT auto-retry without user input.
		log.Print("Sign-in timed out: show \"Try Again\" UI.")
	}
	return nil, err
}
return &response, nil

See the UX guide for the recommended recovery UX.

Supported specs

  • Seedless Restore: passkey-based wallet derivation and discovery
  • Nostr: relay-based event protocol for label storage
  • NIP-42: authentication of clients to relays
  • NIP-65: relay list metadata