# Breez SDK - Spark **→ [About Breez SDK - Spark](/guide/about_breez_sdk_spark.md)** # About Breez SDK - Spark ## **Overview** The Breez SDK provides developers with an end-to-end solution for integrating instant, non-custodial bitcoin and stablecoins into their apps and services. It eliminates the need for third parties, simplifies the complexities of Bitcoin and Lightning, and enables seamless onboarding for billions of users to the future of value transfer. ## **What is the Breez SDK?** It’s a nodeless integration that offers a non-custodial, end-to-end solution for integrating bitcoin and stablecoins, utilizing the Bitcoin-native Layer 2 Lightning & Spark, with on-chain interoperability. Using the Breez SDK, you’ll be able to: - Send and receive bitcoin via Lightning addresses, Bolt11 invoices, LNURL-Pay, bitcoin addresses, and Spark addresses - Send and receive USDC or USDT on Ethereum, Base, Arbitrum, Solana, Tron, and other networks, to and from a bitcoin or USD balance - Issue, send, and receive Spark tokens (BTKN) **Key Features** - [x] Send and receive Lightning payments - [x] Send and receive via LNURL-pay & Lightning addresses - [x] Send and receive USDC/USDT - [x] Send and receive Spark payments (BTC) - [x] Passkey login for seedless experience - [x] Stable Balance - hold your balance in USD - [x] Issue, send and receive Spark tokens (BTKN) - [x] On-chain interoperability - [x] Convert Spark tokens (BTKN) to bitcoin and vice versa - [x] Bindings to all popular languages & frameworks - [x] Keys are only held by users - [x] Multi-app & multi-device support via real-time sync service - [x] Payments persistency including restore support - [x] Automatic claims - [x] WebAssembly support - [x] Compatible with external signers - [x] Full Turnkey integration including client-side signing - [x] Multi-user server mode - [x] Free open-source solution ## Pricing The Breez SDK is **free** for developers. ## Support Have a question for the team? Join us on [Telegram](https://t.me/breezsdk) or email us at . ## API Key The Breez SDK API key must be set for the SDK to work. You can request one by filling out this form or programmatically with the following request: ```bash curl -d "fullname=" -d "company=" -d "email=" -d "message=" \ https://breez.technology/contact/apikey ``` The API key is sent to the provided email address. ## Repository Head over to the Breez SDK - Spark repo. ## Next Steps Follow our step-by-step guide to add the Breez SDK to your app. **→ [Getting Started](/guide/getting_started.md)** **→ [Getting started](/guide/getting_started.md)** # Getting Started Integrating Breez SDK into your application takes just a few minutes. Follow these steps to get started: - **[Installing the SDK](/guide/install.md)** - **[Testing and development](/guide/testing.md)** - **[Initializing the SDK](/guide/initializing.md)** - **[Customizing the SDK](/guide/customizing.md)** - **[Getting the SDK info](/guide/get_info.md)** - **[Listening to events](/guide/events.md)** - **[Adding logging](/guide/logging.md)** - **[Spark status](/guide/spark_status.md)** ## API Key The Breez SDK API key must be set for the SDK to work. You can request one by filling out this form, or programmatically with the following request: ```bash curl -d "fullname=" -d "company=" -d "email=" -d "message=" \ https://breez.technology/contact/apikey ``` The API key is sent to the provided email address. ## UX Guidelines When implementing the Breez SDK, we recommend reading through our [UX Guidelines](/guide/uxguide.md) to provide a consistent and intuitive experience for your end-users. Many of the guidelines are implemented in [Glow](https://glow-app.co), which you can use as a UX reference during SDK implementation. ## Demo Looking for a quick way to try the SDK in your browser or as PWA? Check out our demo app *Glow*: - **Live demo:** [https://glow-app.co](https://glow-app.co) - **Repo:** [breez/breez-sdk-spark-example](https://github.com/breez/breez-sdk-spark-example) > **Note:** The demo is for demonstration purposes only and not intended for production use. ## Support Have a question for the team? Join us on [Telegram](https://t.me/breezsdk) or email us at [contact@breez.technology](mailto:contact@breez.technology). **→ [Installing the SDK](/guide/install.md)** # Installing the SDK The Breez SDK is available on the following platforms: - [iOS/Swift](/guide/install_ios_swift.md) - [Android/Kotlin](/guide/install_android_kotlin.md) - [Kotlin Multiplatform](/guide/install_kotlin_multiplatform.md) - [Javascript/Typescript (Wasm)](/guide/install_javascript.md) - [React Native/Expo](/guide/install_react_native.md) - [Rust](/guide/install_rust.md) - [Flutter](/guide/install_flutter.md) - [Go](/guide/install_go.md) - [Python](/guide/install_python.md) - [C#](/guide/install_csharp.md) **→ [iOS/Swift](/guide/install_ios_swift.md)** # iOS/Swift We support integration via the [Swift Package Manager](https://www.swift.org/package-manager/). See [breez/breez-sdk-spark-swift](https://github.com/breez/breez-sdk-spark-swift) for more information. ## Swift Package Manager ### Installation via Xcode Via `File > Add Packages...`, add ``` https://github.com/breez/breez-sdk-spark-swift.git ``` as a package dependency in Xcode. ### Installation via Swift Package Manifest Add the following to the dependencies array of your `Package.swift`: ``` swift .package(url: "https://github.com/breez/breez-sdk-spark-swift.git", from: "0.25.0"), ``` ## Example App For a full working example app, see the [Swift CLI example app](https://github.com/breez/spark-sdk/tree/main/crates/breez-sdk/bindings/examples/cli/langs/swift). **→ [Android/Kotlin](/guide/install_android_kotlin.md)** # Android/Kotlin We recommend integrating the Breez SDK as Gradle dependency from [our Maven repository](https://mvn.breez.technology/#/releases). To do so, add the following to your Gradle dependencies: ```gradle repositories { maven { url("https://mvn.breez.technology/releases") } } dependencies { implementation("breez_sdk_spark:bindings-android:0.25.0") } ``` ## Example App For a full working example app, see the [Kotlin CLI example app](https://github.com/breez/spark-sdk/tree/main/crates/breez-sdk/bindings/examples/cli/langs/kotlin-multiplatform). **→ [Kotlin Multiplatform](/guide/install_kotlin_multiplatform.md)** # Kotlin Multiplatform We recommend integrating the Breez SDK as Gradle dependency from [our Maven repository](https://mvn.breez.technology/#/releases). Add the Breez Maven repository to your `settings.gradle.kts`: ```gradle pluginManagement { repositories { maven("https://mvn.breez.technology/releases") } } dependencyResolutionManagement { repositories { maven("https://mvn.breez.technology/releases") } } ``` Then add the dependency in your module's `build.gradle.kts`: ```gradle kotlin { sourceSets { commonMain.dependencies { implementation("technology.breez.spark:breez-sdk-spark-kmp:0.25.0") } } } ``` ## Integration ### iOS Install the native binary framework via [Swift Package Manager](#swift-package-manager). The Gradle plugin automatically configures the framework search path from Xcode's build environment. Add the Gradle plugin to your module's `build.gradle.kts` and update the iOS framework binaries to use a dynamic framework: ```gradle plugins { id("technology.breez.spark.kmp") version "0.25.0" } kotlin { listOf( iosArm64(), iosSimulatorArm64(), iosX64(), ).forEach { it.binaries.framework { baseName = "shared" isStatic = false } } } ``` ### macOS Similarly to [iOS](#ios): ```gradle plugins { id("technology.breez.spark.kmp") version "0.25.0" } kotlin { listOf( macosArm64(), macosX64(), ).forEach { it.binaries.framework { baseName = "shared" isStatic = false } } } ``` ### Swift Package Manager **Developer note** `breez-sdk-spark-kmp` Gradle dependency and the Swift package **MUST** have the same version. A version mismatch between the two will cause linking or runtime errors. #### Installation via Xcode Via `File > Add Packages...`, add ``` https://github.com/breez/breez-sdk-spark-swift.git ``` as a package dependency in Xcode. #### Installation via Swift Package Manifest Add the following to the dependencies array of your `Package.swift`: ``` swift .package(url: "https://github.com/breez/breez-sdk-spark-swift.git", from: "0.25.0"), ``` #### Custom Framework Path If the automatic framework detection doesn't work for your setup, you can override it by setting the `breezSdkSparkFrameworkPath` project property to the directory containing `breez_sdk_sparkFFI.framework`. In `gradle.properties`: ```properties breezSdkSparkFrameworkPath=/path/to/framework/dir ``` Or via the command line: ```bash ./gradlew build -PbreezSdkSparkFrameworkPath=/path/to/framework/dir ``` ## Example App For a full working example app, see the [Kotlin Multiplatform CLI example app](https://github.com/breez/spark-sdk/tree/main/crates/breez-sdk/bindings/examples/cli/langs/kotlin-multiplatform). **→ [Javascript/Typescript (Wasm)](/guide/install_javascript.md)** # Javascript/Typescript (Wasm) We recommend using the official npm package: [@breeztech/breez-sdk-spark](https://www.npmjs.com/package/@breeztech/breez-sdk-spark). > **Note:** If using Node.js, the minimum supported version is v22. ```console npm install @breeztech/breez-sdk-spark ``` or ```console yarn add @breeztech/breez-sdk-spark ``` ## Example Apps For full working example apps, see: - [CLI example app](https://github.com/breez/spark-sdk/tree/main/crates/breez-sdk/bindings/examples/cli/langs/wasm) - [Glow Web App](https://github.com/breez/glow-web) **→ [React Native/Expo](/guide/install_react_native.md)** # React Native/Expo Managed Workflow We recommend using the official npm package: [@breeztech/breez-sdk-spark-react-native](https://www.npmjs.com/package/@breeztech/breez-sdk-spark-react-native). ## React Native ```console npm install @breeztech/breez-sdk-spark-react-native ``` or ```console yarn add @breeztech/breez-sdk-spark-react-native ``` ## Expo Managed Workflow ```console npx expo install @breeztech/breez-sdk-spark-react-native ``` Add the plugin to your `app.json` or `app.config.js`: ```json { "expo": { "plugins": [ "@breeztech/breez-sdk-spark-react-native" ] } } ``` ### Plugin Options To enable [Passkey](/guide/passkey_setup.md#ios--macos-apple-app-site-association) support, set `enablePasskey` to `true`. Your app must have the Associated Domains capability enabled. This adds `webcredentials:keys.breez.technology` to the iOS Associated Domains entitlement: ```json { "expo": { "plugins": [ ["@breeztech/breez-sdk-spark-react-native", { "enablePasskey": true }] ] } } ``` **Developer note** This package contains native code and requires a custom development build. It will not work with Expo Go. ## Example App For a full working example app, see the [React Native CLI example app](https://github.com/breez/spark-sdk/tree/main/crates/breez-sdk/bindings/examples/cli/langs/react-native). **→ [Rust](/guide/install_rust.md)** # Rust We recommend to add breez sdk as a git dependency with a specific release tag. Check [breez/spark-sdk](https://github.com/breez/spark-sdk/releases) for the latest version. ```toml [dependencies] breez-sdk-spark = { git = "https://github.com/breez/spark-sdk", tag = "0.25.0" } ``` ## Example App For a full working example app, see the [Rust CLI example app](https://github.com/breez/spark-sdk/tree/main/crates/breez-sdk/cli). **→ [Flutter](/guide/install_flutter.md)** # Flutter We recommend to add our official flutter package as a git dependency. ```yaml dependencies: breez_sdk_spark_flutter: git: url: https://github.com/breez/breez-sdk-spark-flutter ``` ## Example App For a full working example app, see the [Flutter CLI example app](https://github.com/breez/spark-sdk/tree/main/crates/breez-sdk/bindings/examples/cli/langs/flutter). **→ [Go](/guide/install_go.md)** # Go We recommend using our official Go package: [breez/breez-sdk-spark-go](https://github.com/breez/breez-sdk-spark-go). ```console go get github.com/breez/breez-sdk-spark-go ``` ## Integration For [Android](#android) and [Windows](#windows) the provided binding libraries need to be copied into a location where they need to be found during runtime. For [iOS](#ios), the native binary framework must also be installed using [Swift Package Manager](#swift-package-manager). ### Android Copy the binding libraries into the jniLibs directory of your app ```bash cp vendor/github.com/breez/breez-sdk-spark-go/breez_sdk_spark/lib/android-aarch64/*.so android/app/src/main/jniLibs/arm64-v8a/ cp vendor/github.com/breez/breez-sdk-spark-go/breez_sdk_spark/lib/android-amd64/*.so android/app/src/main/jniLibs/x86_64/ ``` So they are in the following structure ``` └── android ├── app └── src └── main └── jniLibs ├── arm64-v8a ├── libbreez_sdk_spark_bindings.so └── libc++_shared.so └── x86_64 ├── libbreez_sdk_spark_bindings.so └── libc++_shared.so └── AndroidManifest.xml └── build.gradle └── build.gradle ``` ### Darwin (macOS) For development, `go run` and `go build` work out of the box since the bundled `.dylib` is referenced via `rpath` pointing into the Go module cache. For deployment, create a universal dylib and place it in your app bundle's Frameworks directory: ```bash lipo -create \ vendor/github.com/breez/breez-sdk-spark-go/breez_sdk_spark/lib/darwin-aarch64/libbreez_sdk_spark_bindings.dylib \ vendor/github.com/breez/breez-sdk-spark-go/breez_sdk_spark/lib/darwin-amd64/libbreez_sdk_spark_bindings.dylib \ -output YourMacOSApp/Contents/Frameworks/libbreez_sdk_spark_bindings.dylib ``` ### iOS Install the native binary framework via [Swift Package Manager](#swift-package-manager). **Developer note** Go and the Swift package **MUST** have the same version. A version mismatch between the two will cause linking or runtime errors. #### Swift Package Manager ##### Installation via Xcode Via `File > Add Packages...`, add ``` https://github.com/breez/breez-sdk-spark-swift.git ``` as a package dependency in Xcode. ##### Installation via Swift Package Manifest Add the following to the dependencies array of your `Package.swift`: ``` swift .package(url: "https://github.com/breez/breez-sdk-spark-swift.git"), ``` ### Windows Copy the binding library to the same directory as the executable file or include the library into the windows install packager. ```bash cp vendor/github.com/breez/breez-sdk-spark-go/breez_sdk_spark/lib/windows-amd64/*.dll build/windows/ ``` ## Example App For a full working example app, see the [Go CLI example app](https://github.com/breez/spark-sdk/tree/main/crates/breez-sdk/bindings/examples/cli/langs/golang). **→ [Python](/guide/install_python.md)** # Python We recommend using our official Python package: [breez-sdk-spark](https://pypi.org/project/breez-sdk-spark). ```console pip install breez-sdk-spark ``` ## Example App For a full working example app, see the [Python CLI example app](https://github.com/breez/spark-sdk/tree/main/crates/breez-sdk/bindings/examples/cli/langs/python). **→ [C#](/guide/install_csharp.md)** # C# We recommend using our official NuGet package: [Breez.Sdk.Spark](https://www.nuget.org/packages/Breez.Sdk.Spark). ## Using .NET CLI ```console dotnet add package Breez.Sdk.Spark ``` ## Using Package Manager Console ```console Install-Package Breez.Sdk.Spark ``` ## Using PackageReference Add the following to your `.csproj` file: ```xml ``` ## Example App For a full working example app, see the [C# CLI example app](https://github.com/breez/spark-sdk/tree/main/crates/breez-sdk/bindings/examples/cli/langs/csharp). **→ [Testing and development](/guide/testing.md)** # Testing and development ## Regtest Network For most testing and development, we recommend using the **Regtest Network** - a deployed test network maintained by Lightspark that is free to use and carries no real-world value. ### What you can test on Regtest - **Spark Payments**: Bitcoin and token payments using the Spark protocol - **Deposits**: Receiving test Bitcoin from the [Lightspark Regtest Faucet](https://app.lightspark.com/regtest-faucet) - **Withdrawals**: Sending funds back to on-chain addresses - **Token Issuance**: Creating and testing tokens using the SDK's [issuing functionality](/guide/issuing_tokens.md) ### Getting started 1. [Initialize the SDK](/guide/initializing.md) using the default regtest config (no API key required) 2. [Generate a Bitcoin receiving address](/guide/receive_payment.md#bitcoin) 3. Request funds from the [faucet](https://app.lightspark.com/regtest-faucet) to your generated address 4. Test all Spark-related functionality in a controlled development environment ## Mainnet testing Some features rely on live networks that Regtest doesn't reproduce. Test these on **Mainnet with small amounts**: use real satoshis, but keep transaction values very low while verifying the flows work correctly. ### Lightning payments The Regtest Network doesn't have a developed Lightning Network, so test Lightning send and receive flows on Mainnet. ### Stable balance and USDC/USDT The stablecoin assets are only available on Mainnet: - **USDB** is the Spark-native stablecoin behind [Stable Balance](/guide/stable_balance.md). - **USDC** and **USDT** are cross-chain assets. Use [USDC/USDT](/guide/cross_chain.md) to pay recipients on their native chains or receive from them. The cross-chain providers operate against live external networks and have no testnet equivalent. Test these integrations on Mainnet with small amounts. ## Development best practices - **Start with Regtest** for most development and testing - **Use Mainnet** for Lightning, stable balance, and USDC/USDT testing - **Test all payment types** you plan to support in your application **→ [Initializing the SDK](/guide/initializing.md)** # Initializing the SDK API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.connect ## Basic Initialization The easiest way to initialize the SDK is with the `connect` method. This method requires: - The network, mnemonic, and Breez API key you intend to use - A storage directory path where the SDK can manage its data **Developer note** For WASM Web, SDK storage is managed using IndexedDB. The storage is used to persist the SDK’s state. If you run multiple SDK instances, each must have its own unique storage directory. Once connected, you’re ready to start interacting with the SDK. ### Rust ```rust // Construct the seed using a mnemonic, entropy or passkey let mnemonic = "".to_string(); let seed = Seed::Mnemonic { mnemonic, passphrase: None, }; // Create the default config let mut config = default_config(Network::Mainnet); config.api_key = Some("".to_string()); // Connect to the SDK using the simplified connect method let sdk = connect(ConnectRequest { config, seed, storage_dir: "./.data".to_string(), }) .await?; ``` ### Swift ```swift // Construct the seed using a mnemonic, entropy or passkey let mnemonic = "" let seed = Seed.mnemonic(mnemonic: mnemonic, passphrase: nil) // Create the default config var config = defaultConfig(network: Network.mainnet) config.apiKey = "" // Connect to the SDK using the simplified connect method let sdk = try await connect( request: ConnectRequest( config: config, seed: seed, storageDir: "./.data" )) ``` ### Kotlin ```kotlin // Construct the seed using a mnemonic, entropy or passkey val mnemonic = "" val seed = Seed.Mnemonic(mnemonic, null) // Create the default config val config = defaultConfig(Network.MAINNET) config.apiKey = "" try { // Connect to the SDK using the simplified connect method val sdk = connect(ConnectRequest( config = config, seed = seed, storageDir = "./.data" )) } catch (e: Exception) { // handle error } ``` ### C# ```csharp // Construct the seed using a mnemonic, entropy or passkey var mnemonic = ""; var seed = new Seed.Mnemonic(mnemonic: mnemonic, passphrase: null); // Create the default config var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { apiKey = "" }; // Connect to the SDK using the simplified connect method var sdk = await BreezSdkSparkMethods.Connect( request: new ConnectRequest( config: config, seed: seed, storageDir: "./.data" ) ); ``` ### Javascript (Wasm) ```typescript // Call init to load the WASM module before calling any other SDK methods. // This is not needed when using the SDK via require() in Node.js. // // For SSR frameworks (Next.js, SvelteKit, Nuxt), use the /ssr subpath: // import init, { connect } from '@breeztech/breez-sdk-spark/ssr' // The /ssr import is safe during server-side rendering. Call init() on the // client only (e.g., inside useEffect or onMount). // // import init from '@breeztech/breez-sdk-spark' await init() // Construct the seed using a mnemonic, entropy or passkey const mnemonic = '' const seed: Seed = { type: 'mnemonic', mnemonic, passphrase: undefined } // Create the default config const config = defaultConfig('mainnet') config.apiKey = '' // Connect to the SDK using the simplified connect method const sdk = await connect({ config, seed, storageDir: './.data' }) ``` ### React Native ```typescript // Construct the seed using a mnemonic, entropy or passkey const mnemonic = '' const seed = new Seed.Mnemonic({ mnemonic, passphrase: undefined }) // Create the default config const config = defaultConfig(Network.Mainnet) config.apiKey = '' const sdk = await connect({ config, seed, storageDir: `${RNFS.DocumentDirectoryPath}/data` }) ``` ### Flutter ```dart // Call once on your Dart entrypoint file, e.g.; `lib/main.dart` // or singleton SDK service. It is recommended to use a single instance // of the SDK across your Flutter app. await BreezSdkSparkLib.init(); // Construct the seed using a mnemonic, entropy or passkey String mnemonic = ""; final seed = Seed.mnemonic(mnemonic: mnemonic, passphrase: null); // Create the default config final config = defaultConfig(network: Network.mainnet) .copyWith(apiKey: ""); final connectRequest = ConnectRequest(config: config, seed: seed, storageDir: "./.data"); final sdk = await connect(request: connectRequest); ``` ### Python ```python # Construct the seed using a mnemonic, entropy or passkey mnemonic = "" seed = Seed.MNEMONIC(mnemonic=mnemonic, passphrase=None) # Create the default config config = default_config(network=Network.MAINNET) config.api_key = "" try: # Connect to the SDK using the simplified connect method sdk = await connect( request=ConnectRequest(config=config, seed=seed, storage_dir="./.data") ) return sdk except Exception as error: logging.error(error) raise ``` ### Go ```go // Construct the seed using a mnemonic, entropy or passkey mnemonic := "" var seed breez_sdk_spark.Seed = breez_sdk_spark.SeedMnemonic{ Mnemonic: mnemonic, Passphrase: nil, } // Create the default config apiKey := "" config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) config.ApiKey = &apiKey connectRequest := breez_sdk_spark.ConnectRequest{ Config: config, Seed: seed, StorageDir: "./.data", } // Connect to the SDK using the simplified connect method sdk, err := breez_sdk_spark.Connect(connectRequest) return sdk, err ``` **Developer note** On some platforms (e.g., Android, iOS), you must use an application-specific writable directory within the app's sandbox for SDK storage. ### Connecting with a Passkey Instead of managing mnemonics directly, you can use passkeys to derive wallet seeds deterministically. This eliminates the need for mnemonic backup and provides a seamless authentication experience using biometrics or device PIN. See [Connecting with a Passkey](/guide/passkey.md) for the full setup guide including PRF provider implementation, platform configuration, and label management. ## Advanced Initialization If you're building a multi-tenant server deployment, start with [Server mode](/guide/server_mode.md) for the recommended profile, lifecycle pattern, and shared-infrastructure wiring. For advanced use cases where you need more control, you can configure the SDK using the Builder pattern. With the SDK Builder you can define: - [Storage](/guide/customizing.md#with-storage) to manage stored data - [Bitcoin Chain Service](/guide/customizing.md#with-chain-service) to provide network data - [LNURL Client](/guide/customizing.md#with-lnurl-client) to make REST requests - [Fiat Service](/guide/customizing.md#with-fiat-service) to provide Fiat currencies and exchange rates - Change the [Account Number](/guide/customizing.md#with-account-number) to derive an independent wallet from the same seed - [Payment Observer](/guide/customizing.md#with-payment-observer) to be notified before payments occur See [Customizing the SDK](/guide/customizing.md) for examples of this advanced initialization pattern. ## Disconnecting API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.disconnect When you’re done using the SDK, call the disconnect method to release any resources in use. This is particularly useful if you need to re-instantiate the SDK, such as when changing the mnemonic or updating configuration. ### Rust ```rust pub(crate) async fn disconnect(sdk: &BreezSdk) -> Result<()> { sdk.disconnect().await?; Ok(()) } ``` ### Swift ```swift func disconnect(sdk: BreezSdk) async throws { try await sdk.disconnect() } ``` ### Kotlin ```kotlin suspend fun disconnect(sdk: BreezSdk) { try { sdk.disconnect() } catch (e: Exception) { // handle error } } ``` ### C# ```csharp async Task Disconnect(BreezSdk sdk) { await sdk.Disconnect(); } ``` ### Javascript (Wasm) ```typescript await sdk.disconnect() ``` ### React Native ```typescript await sdk.disconnect() ``` ### Flutter ```dart Future disconnect(BreezSdk sdk) async { await sdk.disconnect(); } ``` ### Python ```python async def disconnect(sdk: BreezSdk): try: await sdk.disconnect() except Exception as error: logging.error(error) raise ``` ### Go ```go func Disconnect(sdk *breez_sdk_spark.BreezSdk) { sdk.Disconnect() } ``` --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Customizing the SDK](/guide/customizing.md)** # Customizing the SDK Using the SDK Builder gives you more control over the initialization and modular components used when the SDK is running. Below you can find examples of initializing the SDK using the SDK Builder and implementing modular components. The shared-pool, shared-chain-service, and shared-connection-manager components on this page are designed for multi-tenant server deployments — they're most useful in combination with the [Server mode](/guide/server_mode.md) SDK profile. - [Storage](#with-storage) to manage stored data - [PostgreSQL Backend](#with-postgres-backend) as an alternative storage backend - [MySQL Backend](#with-mysql-backend) as an alternative storage backend - [Bitcoin Chain Service](#with-chain-service) to provide network data - [Shared REST Chain Service](#with-shared-rest-chain-service) to share the chain service HTTP client across SDK instances - [LNURL Client](#with-lnurl-client) to make REST requests - [Fiat Service](#with-fiat-service) to provide Fiat currencies and exchange rates - Change the [Account Number](#with-account-number) to derive an independent wallet from the same seed - [Payment Observer](#with-payment-observer) to be notified before payments occur - [Session Store](#with-session-store) to customize how cached auth tokens are persisted (for example, at-rest encryption) - [Shared SDK Context](#with-shared-context) to share connection pools and HTTP/gRPC clients across SDK instances ## Rust ```rust // Construct the seed using a mnemonic, entropy or passkey let mnemonic = "".to_string(); let seed = Seed::Mnemonic { mnemonic, passphrase: None, }; // Create the default config let mut config = default_config(Network::Mainnet); config.api_key = Some("".to_string()); // Build the SDK using the config, seed and default storage let builder = SdkBuilder::new(config, seed).with_default_storage("./.data".to_string()); // You can also pass your custom implementations: // let builder = builder.with_storage_backend(custom_storage()) // let builder = builder.with_chain_service() // let builder = builder.with_rest_client() // let builder = builder.with_account_number() // let builder = builder.with_payment_observer(); let sdk = builder.build().await?; ``` ## Swift ```swift // Construct the seed using a mnemonic, entropy or passkey let mnemonic = "" let seed = Seed.mnemonic(mnemonic: mnemonic, passphrase: nil) // Create the default config var config = defaultConfig(network: Network.mainnet) config.apiKey = "" // Build the SDK using the config, seed and default storage let builder = SdkBuilder(config: config, seed: seed) await builder.withDefaultStorage(storageDir: "./.data") // You can also pass your custom implementations: // await builder.withStorage() // await builder.withChainService() // await builder.withRestClient() // await builder.withAccountNumber(accountNumber: ) // await builder.withPaymentObserver() let sdk = try await builder.build() ``` ## Kotlin ```kotlin // Construct the seed using a mnemonic, entropy or passkey val mnemonic = "" val seed = Seed.Mnemonic(mnemonic, null) // Create the default config val config = defaultConfig(Network.MAINNET) config.apiKey = "" try { // Build the SDK using the config, seed and default storage val builder = SdkBuilder(config, seed) builder.withDefaultStorage("./.data") // You can also pass your custom implementations: // builder.withStorage() // builder.withChainService() // builder.withRestClient() // builder.withAccountNumber() // builder.withPaymentObserver() val sdk = builder.build() } catch (e: Exception) { // handle error } ``` ## C# ```csharp // Construct the seed using a mnemonic, entropy or passkey var mnemonic = ""; var seed = new Seed.Mnemonic(mnemonic: mnemonic, passphrase: null); // Create the default config var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { apiKey = "" }; // Build the SDK using the config, seed and default storage var builder = new SdkBuilder(config: config, seed: seed); await builder.WithDefaultStorage(storageDir: "./.data"); // You can also pass your custom implementations: // await builder.WithStorage() // await builder.WithChainService() // await builder.WithRestClient() // await builder.WithAccountNumber() // await builder.WithPaymentObserver(); var sdk = await builder.Build(); ``` ## Javascript (Wasm) ```typescript // Call init when using the SDK in a web environment before calling any other SDK // methods. This is not needed when using the SDK in a Node.js/Deno environment. await init() // Construct the seed using a mnemonic, entropy or passkey const mnemonic = '' const seed: Seed = { type: 'mnemonic', mnemonic, passphrase: undefined } // Create the default config const config = defaultConfig('mainnet') config.apiKey = '' // Build the SDK using the config, seed and default storage let builder = SdkBuilder.new(config, seed) builder = await builder.withDefaultStorage('./.data') // You can also pass your custom implementations: // builder = builder.withStorage() // builder = builder.withChainService() // builder = builder.withRestClient() // builder = builder.withAccountNumber() // builder = builder.withPaymentObserver() const sdk = await builder.build() ``` ## React Native ```typescript // Construct the seed using a mnemonic, entropy or passkey const mnemonic = '' const seed = new Seed.Mnemonic({ mnemonic, passphrase: undefined }) // Create the default config const config = defaultConfig(Network.Mainnet) config.apiKey = '' // Build the SDK using the config, seed and default storage const builder = new SdkBuilder(config, seed) await builder.withDefaultStorage(`${RNFS.DocumentDirectoryPath}/data`) // You can also pass your custom implementations: // await builder.withStorage() // await builder.withChainService() // await builder.withRestClient() // await builder.withAccountNumber() // await builder.withPaymentObserver() const sdk = await builder.build() ``` ## Flutter ```dart // Construct the seed using a mnemonic, entropy or passkey String mnemonic = ""; final seed = Seed.mnemonic(mnemonic: mnemonic, passphrase: null); // Create the default config final config = defaultConfig(network: Network.mainnet) .copyWith(apiKey: ""); // Build the SDK using the config, seed and default storage final builder = SdkBuilder(config: config, seed: seed); builder.withDefaultStorage(storageDir: "./.data"); // You can also pass your custom implementations: // builder.withRestChainService( // url: "https://custom.chain.service", // credentials: Credentials( // username: "service-username", password: "service-password")); // builder.withAccountNumber(accountNumber: ); final sdk = await builder.build(); ``` ## Python ```python # Construct the seed using a mnemonic, entropy or passkey mnemonic = "" seed = Seed.MNEMONIC(mnemonic=mnemonic, passphrase=None) # Create the default config config = default_config(network=Network.MAINNET) config.api_key = "" try: # Build the SDK using the config, seed and default storage builder = SdkBuilder(config=config, seed=seed) await builder.with_default_storage(storage_dir="./.data") # You can also pass your custom implementations: # await builder.with_storage() # await builder.with_chain_service() # await builder.with_rest_client() # await builder.with_account_number() # await builder.with_payment_observer() sdk = await builder.build() return sdk except Exception as error: logging.error(error) raise ``` ## Go ```go // Construct the seed using a mnemonic, entropy or passkey mnemonic := "" var seed breez_sdk_spark.Seed = breez_sdk_spark.SeedMnemonic{ Mnemonic: mnemonic, Passphrase: nil, } // Create the default config apiKey := "" config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) config.ApiKey = &apiKey // Build the SDK using the config, seed and default storage builder := breez_sdk_spark.NewSdkBuilder(config, seed) builder.WithDefaultStorage("./.data") // You can also pass your custom implementations: // builder.WithStorage() // builder.WithChainService() // builder.WithRestClient() // builder.WithAccountNumber() // builder.WithPaymentObserver() sdk, err := builder.Build() return sdk, err ``` ## With Storage API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.SdkBuilder.html#method.with_storage When using the SDK Builder, you either have to provide a Storage implementation or use the default storage from the SDK. **Note:** Flutter currently only supports using the default storage. ## With PostgreSQL Backend API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.SdkBuilder.html#method.with_storage_backend The SDK includes a PostgreSQL backend as an alternative to file-based storage. Build a storage config with `postgres_storage` and pass it to the builder via `with_storage_backend` — this configures PostgreSQL for all stores (storage, tree store, and token store), which is suitable for server-side deployments with horizontal scaling. To share a single connection pool across multiple SDK instances, supply the same config through a [Shared SDK Context](#with-shared-context); per-tenant scoping (rows isolated by seed identity) is preserved either way. If your service owns the database schema and applies SDK-compatible migrations externally, set `run_migration` to `false` on the storage config. The SDK will trust the existing schema and skip all migration runs, including writes to schema migration tables. **Note:** Not available for React Native or Flutter. For JavaScript/TypeScript, only supported in Node.js (not in the browser). ### Rust ```rust // Construct the seed using a mnemonic, entropy or passkey let mnemonic = "".to_string(); let seed = Seed::Mnemonic { mnemonic, passphrase: None, }; // Create the default config let mut config = default_config(Network::Mainnet); config.api_key = Some("".to_string()); // Configure PostgreSQL backend // Connection string format: "host=localhost user=postgres password=secret dbname=spark" // Or URI format: "postgres://user:password@host:port/dbname?sslmode=require" // TLS: "sslmode=require" encrypts and verifies the server certificate let mut postgres_config = default_postgres_storage_config("host=localhost user=postgres dbname=spark".to_string()); // Optionally pool settings can be adjusted. Some examples: postgres_config.max_pool_size = 8; // Max connections in pool postgres_config.wait_timeout_secs = Some(30); // Timeout waiting for connection // If your service owns SDK-compatible schema migrations: postgres_config.run_migration = false; // Build the SDK with the PostgreSQL storage backend (storage, tree store, // and token store). Per-tenant scoping (rows isolated by seed identity) is // applied automatically. let sdk = SdkBuilder::new(config, seed) .with_storage_backend(postgres_storage(postgres_config)?) .build() .await?; ``` ### Swift ```swift // Construct the seed using a mnemonic, entropy or passkey let mnemonic = "" let seed = Seed.mnemonic(mnemonic: mnemonic, passphrase: nil) // Create the default config var config = defaultConfig(network: Network.mainnet) config.apiKey = "" // Configure PostgreSQL backend // Connection string format: "host=localhost user=postgres password=secret dbname=spark" // Or URI format: "postgres://user:password@host:port/dbname?sslmode=require" // TLS: "sslmode=require" encrypts and verifies the server certificate var postgresConfig = defaultPostgresStorageConfig( connectionString: "host=localhost user=postgres dbname=spark" ) // Optionally pool settings can be adjusted. Some examples: postgresConfig.maxPoolSize = UInt32(8) // Max connections in pool postgresConfig.waitTimeoutSecs = UInt64(30) // Timeout waiting for connection // If your service owns SDK-compatible schema migrations: postgresConfig.runMigration = false // Build the SDK with the PostgreSQL storage backend (storage, tree store, // and token store). Per-tenant scoping (rows isolated by seed identity) // is applied automatically. let builder = SdkBuilder(config: config, seed: seed) await builder.withStorageBackend( storage: try postgresStorage(config: postgresConfig)) let sdk = try await builder.build() ``` ### Kotlin ```kotlin // Construct the seed using a mnemonic, entropy or passkey val mnemonic = "" val seed = Seed.Mnemonic(mnemonic, null) // Create the default config val config = defaultConfig(Network.MAINNET) config.apiKey = "" // Configure PostgreSQL backend // Connection string format: "host=localhost user=postgres password=secret dbname=spark" // Or URI format: "postgres://user:password@host:port/dbname?sslmode=require" // TLS: "sslmode=require" encrypts and verifies the server certificate val postgresConfig = defaultPostgresStorageConfig("host=localhost user=postgres dbname=spark") // Optionally pool settings can be adjusted. Some examples: postgresConfig.maxPoolSize = 8u // Max connections in pool postgresConfig.waitTimeoutSecs = 30u // Timeout waiting for connection // If your service owns SDK-compatible schema migrations: postgresConfig.runMigration = false try { // Build the SDK with the PostgreSQL storage backend (storage, tree // store, and token store). Per-tenant scoping (rows isolated by // seed identity) is applied automatically. val builder = SdkBuilder(config, seed) builder.withStorageBackend(postgresStorage(postgresConfig)) val sdk = builder.build() } catch (e: Exception) { // handle error } ``` ### C# ```csharp // Construct the seed using a mnemonic, entropy or passkey var mnemonic = ""; var seed = new Seed.Mnemonic(mnemonic: mnemonic, passphrase: null); // Create the default config var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { apiKey = "" }; // Configure PostgreSQL backend // Connection string format: "host=localhost user=postgres password=secret dbname=spark" // Or URI format: "postgres://user:password@host:port/dbname?sslmode=require" // TLS: "sslmode=require" encrypts and verifies the server certificate var postgresConfig = BreezSdkSparkMethods.DefaultPostgresStorageConfig( connectionString: "host=localhost user=postgres dbname=spark" ); // Optionally pool settings can be adjusted. Some examples: postgresConfig = postgresConfig with { maxPoolSize = 8u, // Max connections in pool waitTimeoutSecs = 30ul, // Timeout waiting for connection // If your service owns SDK-compatible schema migrations: runMigration = false }; // Build the SDK with the PostgreSQL storage backend (storage, tree // store, and token store). Per-tenant scoping (rows isolated by // seed identity) is applied automatically. var builder = new SdkBuilder(config: config, seed: seed); await builder.WithStorageBackend( storage: BreezSdkSparkMethods.PostgresStorage(postgresConfig) ); var sdk = await builder.Build(); ``` ### Javascript (Wasm) ```typescript // Construct the seed using a mnemonic, entropy or passkey const mnemonic = '' const seed: Seed = { type: 'mnemonic', mnemonic, passphrase: undefined } // Create the default config const config = defaultConfig('mainnet') config.apiKey = '' // Configure PostgreSQL backend // Connection string format: "host=localhost user=postgres password=secret dbname=spark" // Or URI format: "postgres://user:password@host:port/dbname?sslmode=require" // TLS: "sslmode=require" encrypts and verifies the server certificate const pgConfig = defaultPostgresStorageConfig('host=localhost user=postgres dbname=spark') // Optionally pool settings can be adjusted. Some examples: pgConfig.maxPoolSize = 8 // Max connections in pool pgConfig.createTimeoutSecs = 30 // Timeout for establishing a new connection pgConfig.recycleTimeoutSecs = 30 // Timeout for recycling an idle connection // If your service owns SDK-compatible schema migrations: pgConfig.runMigration = false // Build the SDK with the PostgreSQL storage backend (storage, tree store, // and token store). Per-tenant scoping (rows isolated by seed identity) is // applied automatically. let builder = SdkBuilder.new(config, seed) builder = builder.withStorageBackend(postgresStorage(pgConfig)) const sdk = await builder.build() ``` ### Python ```python async def init_sdk_postgres(): # Construct the seed using a mnemonic, entropy or passkey mnemonic = "" seed = Seed.MNEMONIC(mnemonic=mnemonic, passphrase=None) # Create the default config config = default_config(network=Network.MAINNET) config.api_key = "" # Configure PostgreSQL storage # Connection string format: "host=localhost user=postgres password=secret dbname=spark" # Or URI format: "postgres://user:password@host:port/dbname?sslmode=require" # TLS: "sslmode=require" encrypts and verifies the server certificate postgres_config = default_postgres_storage_config( connection_string="host=localhost user=postgres dbname=spark" ) # Optionally pool settings can be adjusted. Some examples: postgres_config.max_pool_size = 8 # Max connections in pool postgres_config.wait_timeout_secs = 30 # Timeout waiting for connection # If your service owns SDK-compatible schema migrations: postgres_config.run_migration = False try: # Build the SDK with the PostgreSQL storage backend (storage, tree # store, and token store). Per-tenant scoping (rows isolated by seed # identity) is applied automatically. builder = SdkBuilder(config=config, seed=seed) await builder.with_storage_backend( storage=postgres_storage(config=postgres_config) ) sdk = await builder.build() return sdk except Exception as error: logging.error(error) raise ``` ### Go ```go // Construct the seed using a mnemonic, entropy or passkey mnemonic := "" var seed breez_sdk_spark.Seed = breez_sdk_spark.SeedMnemonic{ Mnemonic: mnemonic, Passphrase: nil, } // Create the default config apiKey := "" config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) config.ApiKey = &apiKey // Configure PostgreSQL backend // Connection string format: "host=localhost user=postgres password=secret dbname=spark" // Or URI format: "postgres://user:password@host:port/dbname?sslmode=require" // TLS: "sslmode=require" encrypts and verifies the server certificate postgresConfig := breez_sdk_spark.DefaultPostgresStorageConfig( "host=localhost user=postgres dbname=spark", ) // Optionally pool settings can be adjusted. Some examples: postgresConfig.MaxPoolSize = 8 // Max connections in pool waitTimeoutSecs := uint64(30) postgresConfig.WaitTimeoutSecs = &waitTimeoutSecs // Timeout waiting for connection // If your service owns SDK-compatible schema migrations: postgresConfig.RunMigration = false // Build the SDK with the PostgreSQL storage backend (storage, tree store, // and token store). Per-tenant scoping (rows isolated by seed identity) // is applied automatically. builder := breez_sdk_spark.NewSdkBuilder(config, seed) storageBackend, err := breez_sdk_spark.PostgresStorage(postgresConfig) if err != nil { return nil, err } builder.WithStorageBackend(storageBackend) sdk, err := builder.Build() if err != nil { return nil, err } ``` **Developer note** TLS is controlled by the `sslmode` connection-string parameter. For production, set `sslmode=require`: it encrypts the connection and verifies the server certificate. `verify-ca` and `verify-full` are also supported, and `no-verify` is the explicit opt-in for TLS without certificate verification (for example, a self-signed certificate you cannot add to a trust store). When `sslmode` is absent, TLS is used when the server supports it and is always verified; the exception is JavaScript/TypeScript on Node.js, where an absent `sslmode` means no TLS. Servers using a private CA are trusted via `root_ca_pem` on the storage config, or on Node.js via the `sslrootcert=` URI parameter or Node's trust store (for example, the `NODE_EXTRA_CA_CERTS` environment variable). `verify-ca` performs chain verification without a hostname check and requires a pinned CA: `root_ca_pem` on the storage config, or on Node.js the `sslrootcert=` URI parameter. Use it when hostname verification cannot succeed, such as connecting to the server by IP address. Sharing the same PostgreSQL database with multiple SDK instances is incompatible with real-time sync. See [Real-time sync server URL](/guide/config.md#real-time-sync-server-url) for how to disable it. The PostgreSQL tree store can use the same or a separate PostgreSQL database as the PostgreSQL storage. The tree store uses its own set of tables prefixed with `tree_`. ## With MySQL Backend API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.SdkBuilder.html#method.with_storage_backend The SDK includes a MySQL backend (MySQL 8.0+) as an alternative to file-based storage. Build a storage config with `mysql_storage` and pass it to the builder via `with_storage_backend` — this configures MySQL for all stores (storage, tree store, and token store), which is suitable for server-side deployments with horizontal scaling. To share a single connection pool across multiple SDK instances, supply the same config through a [Shared SDK Context](#with-shared-context); per-tenant scoping (rows isolated by seed identity) is preserved either way. If your service owns the database schema and applies SDK-compatible migrations externally, set `run_migration` to `false` on the storage config. The SDK will trust the existing schema and skip all migration runs, including writes to schema migration tables. **Note:** Not available for React Native or Flutter. For JavaScript/TypeScript, only supported in Node.js (not in the browser). ### Rust ```rust // Construct the seed using a mnemonic, entropy or passkey let mnemonic = "".to_string(); let seed = Seed::Mnemonic { mnemonic, passphrase: None, }; // Create the default config let mut config = default_config(Network::Mainnet); config.api_key = Some("".to_string()); // Configure MySQL backend (MySQL 8.0+). // Connection string format (URL only): // "mysql://user:password@host:3306/dbname?ssl-mode=required" // TLS: "ssl-mode=required" encrypts and verifies the server certificate let mut mysql_config = default_mysql_storage_config("mysql://user:password@localhost:3306/spark".to_string()); // Optionally pool settings can be adjusted. Some examples: mysql_config.max_pool_size = 8; // Max connections in pool mysql_config.recycle_timeout_secs = Some(60); // Recycle idle connections after this many seconds // Provide a custom CA certificate when the server uses a private CA: // mysql_config.root_ca_pem = Some("-----BEGIN CERTIFICATE-----\n...".to_string()); // Build the SDK with the MySQL storage backend (storage, tree store, and // token store). Per-tenant scoping (rows isolated by seed identity) is // applied automatically. let sdk = SdkBuilder::new(config, seed) .with_storage_backend(mysql_storage(mysql_config)?) .build() .await?; ``` ### Swift ```swift // Construct the seed using a mnemonic, entropy or passkey let mnemonic = "" let seed = Seed.mnemonic(mnemonic: mnemonic, passphrase: nil) // Create the default config var config = defaultConfig(network: Network.mainnet) config.apiKey = "" // Configure MySQL backend (MySQL 8.0+). // Connection string format (URL only): // "mysql://user:password@host:3306/dbname?ssl-mode=required" // TLS: "ssl-mode=required" encrypts and verifies the server certificate var mysqlConfig = defaultMysqlStorageConfig( connectionString: "mysql://user:password@localhost:3306/spark" ) // Optionally pool settings can be adjusted. Some examples: mysqlConfig.maxPoolSize = UInt32(8) // Max connections in pool mysqlConfig.recycleTimeoutSecs = UInt64(60) // Recycle idle connections after this many seconds // Provide a custom CA certificate when the server uses a private CA: // mysqlConfig.rootCaPem = "-----BEGIN CERTIFICATE-----\n..." // Build the SDK with the MySQL storage backend (storage, tree store, and // token store). Per-tenant scoping (rows isolated by seed identity) is // applied automatically. let builder = SdkBuilder(config: config, seed: seed) await builder.withStorageBackend( storage: try mysqlStorage(config: mysqlConfig)) let sdk = try await builder.build() ``` ### Kotlin ```kotlin // Construct the seed using a mnemonic, entropy or passkey val mnemonic = "" val seed = Seed.Mnemonic(mnemonic, null) // Create the default config val config = defaultConfig(Network.MAINNET) config.apiKey = "" // Configure MySQL backend (MySQL 8.0+). // Connection string format (URL only): // "mysql://user:password@host:3306/dbname?ssl-mode=required" // TLS: "ssl-mode=required" encrypts and verifies the server certificate val mysqlConfig = defaultMysqlStorageConfig("mysql://user:password@localhost:3306/spark") // Optionally pool settings can be adjusted. Some examples: mysqlConfig.maxPoolSize = 8u // Max connections in pool mysqlConfig.recycleTimeoutSecs = 60u // Recycle idle connections after this many seconds // Provide a custom CA certificate when the server uses a private CA: // mysqlConfig.rootCaPem = "-----BEGIN CERTIFICATE-----\n..." try { // Build the SDK with the MySQL storage backend (storage, tree // store, and token store). Per-tenant scoping (rows isolated by // seed identity) is applied automatically. val builder = SdkBuilder(config, seed) builder.withStorageBackend(mysqlStorage(mysqlConfig)) val sdk = builder.build() } catch (e: Exception) { // handle error } ``` ### C# ```csharp // Construct the seed using a mnemonic, entropy or passkey var mnemonic = ""; var seed = new Seed.Mnemonic(mnemonic: mnemonic, passphrase: null); // Create the default config var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { apiKey = "" }; // Configure MySQL backend (MySQL 8.0+). // Connection string format (URL only): // "mysql://user:password@host:3306/dbname?ssl-mode=required" // TLS: "ssl-mode=required" encrypts and verifies the server certificate var mysqlConfig = BreezSdkSparkMethods.DefaultMysqlStorageConfig( connectionString: "mysql://user:password@localhost:3306/spark" ); // Optionally pool settings can be adjusted. Some examples: mysqlConfig = mysqlConfig with { maxPoolSize = 8u, // Max connections in pool recycleTimeoutSecs = 60ul // Recycle idle connections after this many seconds }; // Provide a custom CA certificate when the server uses a private CA: // mysqlConfig = mysqlConfig with { rootCaPem = "-----BEGIN CERTIFICATE-----\n..." }; // Build the SDK with the MySQL storage backend (storage, tree // store, and token store). Per-tenant scoping (rows isolated by // seed identity) is applied automatically. var builder = new SdkBuilder(config: config, seed: seed); await builder.WithStorageBackend( storage: BreezSdkSparkMethods.MysqlStorage(mysqlConfig) ); var sdk = await builder.Build(); ``` ### Javascript (Wasm) ```typescript // Construct the seed using a mnemonic, entropy or passkey const mnemonic = '' const seed: Seed = { type: 'mnemonic', mnemonic, passphrase: undefined } // Create the default config const config = defaultConfig('mainnet') config.apiKey = '' // Configure MySQL backend (MySQL 8.0+). // Connection string format (URL only): // "mysql://user:password@host:3306/dbname?ssl-mode=required" // TLS: "ssl-mode=required" encrypts and verifies the server certificate const mysqlConfig = defaultMysqlStorageConfig('mysql://user:password@localhost:3306/spark') // Optionally pool settings can be adjusted. Some examples: mysqlConfig.maxPoolSize = 8 // Max connections in pool mysqlConfig.createTimeoutSecs = 30 // Timeout for establishing a new connection mysqlConfig.recycleTimeoutSecs = 60 // Recycle idle connections after this many seconds // To trust a private CA, add it to Node's trust store // (e.g. via the NODE_EXTRA_CA_CERTS environment variable) // Build the SDK with the MySQL storage backend (storage, tree store, and // token store). Per-tenant scoping (rows isolated by seed identity) is // applied automatically. let builder = SdkBuilder.new(config, seed) builder = builder.withStorageBackend(mysqlStorage(mysqlConfig)) const sdk = await builder.build() ``` ### Python ```python async def init_sdk_mysql(): # Construct the seed using a mnemonic, entropy or passkey mnemonic = "" seed = Seed.MNEMONIC(mnemonic=mnemonic, passphrase=None) # Create the default config config = default_config(network=Network.MAINNET) config.api_key = "" # Configure MySQL backend (MySQL 8.0+). # Connection string format (URL only): # "mysql://user:password@host:3306/dbname?ssl-mode=required" # TLS: "ssl-mode=required" encrypts and verifies the server certificate mysql_config = default_mysql_storage_config( connection_string="mysql://user:password@localhost:3306/spark" ) # Optionally pool settings can be adjusted. Some examples: mysql_config.max_pool_size = 8 # Max connections in pool mysql_config.recycle_timeout_secs = 60 # Recycle idle connections after this many seconds # Provide a custom CA certificate when the server uses a private CA: # mysql_config.root_ca_pem = "-----BEGIN CERTIFICATE-----\n..." try: # Build the SDK with the MySQL storage backend (storage, tree store, # and token store). Per-tenant scoping (rows isolated by seed identity) # is applied automatically. builder = SdkBuilder(config=config, seed=seed) await builder.with_storage_backend( storage=mysql_storage(config=mysql_config) ) sdk = await builder.build() return sdk except Exception as error: logging.error(error) raise ``` ### Go ```go // Construct the seed using a mnemonic, entropy or passkey mnemonic := "" var seed breez_sdk_spark.Seed = breez_sdk_spark.SeedMnemonic{ Mnemonic: mnemonic, Passphrase: nil, } // Create the default config apiKey := "" config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) config.ApiKey = &apiKey // Configure MySQL backend (MySQL 8.0+). // Connection string format (URL only): // "mysql://user:password@host:3306/dbname?ssl-mode=required" // TLS: "ssl-mode=required" encrypts and verifies the server certificate mysqlConfig := breez_sdk_spark.DefaultMysqlStorageConfig( "mysql://user:password@localhost:3306/spark", ) // Optionally pool settings can be adjusted. Some examples: mysqlConfig.MaxPoolSize = 8 // Max connections in pool recycleTimeoutSecs := uint64(60) // Recycle idle connections after this many seconds mysqlConfig.RecycleTimeoutSecs = &recycleTimeoutSecs // Provide a custom CA certificate when the server uses a private CA: // rootCa := "-----BEGIN CERTIFICATE-----\n..." // mysqlConfig.RootCaPem = &rootCa // Build the SDK with the MySQL storage backend (storage, tree store, and // token store). Per-tenant scoping (rows isolated by seed identity) is // applied automatically. builder := breez_sdk_spark.NewSdkBuilder(config, seed) storageBackend, err := breez_sdk_spark.MysqlStorage(mysqlConfig) if err != nil { return nil, err } builder.WithStorageBackend(storageBackend) sdk, err := builder.Build() if err != nil { return nil, err } ``` **Developer note** MySQL only accepts URL-form connection strings (`mysql://user:password@host:3306/dbname`); the key=value form supported by PostgreSQL is not available. TLS is controlled by the `ssl-mode` URL parameter, with the same spellings on every platform: `required` (recommended for production) and `verify_identity` verify the server certificate chain and hostname, `verify_ca` verifies the chain only, and `no-verify` is the explicit opt-in for TLS without certificate verification. An absent `ssl-mode` means no TLS. Servers using a private CA are trusted via `root_ca_pem` on the storage config, or on JavaScript/TypeScript (Node.js) via the `ssl-ca=` URL parameter or Node's trust store (for example, the `NODE_EXTRA_CA_CERTS` environment variable). `verify_ca` performs chain verification without a hostname check and requires a pinned CA: `root_ca_pem` on the storage config, or on Node.js the `ssl-ca=` URL parameter. Use it when hostname verification cannot succeed, such as connecting to the server by IP address. Sharing the same MySQL database with multiple SDK instances is incompatible with real-time sync. See [Real-time sync server URL](/guide/config.md#real-time-sync-server-url) for how to disable it. The MySQL tree store can use the same or a separate MySQL database as the MySQL storage. The tree store uses its own set of tables prefixed with `tree_`. ## With Chain Service API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.SdkBuilder.html#method.with_chain_service The SDK provides a default Bitcoin Chain Service implementation. If you want to use your own, you can provide it either by using [With REST Chain Service](#with-rest-chain-service) or by implementing the Bitcoin Chain Service interface. ## With REST Chain Service API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.SdkBuilder.html#method.with_rest_chain_service The SDK provides a default Bitcoin Chain Service implementation. If you want to use your own, you can provide it either by using [With Chain Service](#with-chain-service) or by providing a URL and optional credentials. ### Rust ```rust let url = "".to_string(); let chain_api_type = ChainApiType::MempoolSpace; let optional_credentials = Credentials { username: "".to_string(), password: "".to_string(), }; builder.with_rest_chain_service(url, chain_api_type, Some(optional_credentials)) ``` ### Swift ```swift let url = "" let chainApiType = ChainApiType.mempoolSpace let optionalCredentials = Credentials( username: "", password: "" ) await builder.withRestChainService( url: url, apiType: chainApiType, credentials: optionalCredentials ) ``` ### Kotlin ```kotlin val url = "" val chainApiType = ChainApiType.MEMPOOL_SPACE val optionalCredentials = Credentials( username = "", password = "" ) builder.withRestChainService( url = url, apiType = chainApiType, credentials = optionalCredentials ) ``` ### C# ```csharp var url = ""; var chainApiType = ChainApiType.MempoolSpace; var optionalCredentials = new Credentials( username: "", password: "" ); await builder.WithRestChainService( url: url, apiType: chainApiType, credentials: optionalCredentials ); ``` ### Javascript (Wasm) ```typescript const url = '' const chainApiType = 'mempoolSpace' const optionalCredentials: Credentials = { username: '', password: '' } builder = builder.withRestChainService(url, chainApiType, optionalCredentials) ``` ### React Native ```typescript const url = '' const chainApiType = ChainApiType.MempoolSpace const optionalCredentials: Credentials = { username: '', password: '' } await builder.withRestChainService(url, chainApiType, optionalCredentials) ``` ### Flutter ```dart String url = ""; var chainApiType = ChainApiType.mempoolSpace; var optionalCredentials = Credentials( username: "", password: "", ); builder.withRestChainService( url: url, apiType: chainApiType, credentials: optionalCredentials, ); ``` ### Python ```python url = "" chain_api_type = ChainApiType.MEMPOOL_SPACE optional_credentials = Credentials( username="", password="", ) await builder.with_rest_chain_service( url=url, api_type=chain_api_type, credentials=optional_credentials, ) ``` ### Go ```go url := "" chainApiType := breez_sdk_spark.ChainApiTypeMempoolSpace optionalCredentials := &breez_sdk_spark.Credentials{ Username: "", Password: "", } builder.WithRestChainService(url, chainApiType, optionalCredentials) ``` ## With Shared REST Chain Service API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/fn.new_rest_chain_service.html [With REST Chain Service](#with-rest-chain-service) builds a fresh chain service inside each SDK instance. Server processes hosting many wallets at once can share a single REST chain service between every SDK, so they reuse the same pooled HTTP client (and its HTTP/2 connection pool) instead of each opening a fresh one. Construct one via `new_rest_chain_service` and pass it to each `SdkBuilder` via `with_chain_service`. All SDK instances sharing the chain service must be configured for the same network. ## With Fiat Service API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.SdkBuilder.html#method.with_fiat_service The SDK by default provides a list of available Fiat currencies and current exchange rates. If you want to use your own, you can provide it by implementing the Fiat Service interface. ## With LNURL Client API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.SdkBuilder.html#method.with_lnurl_client The LNURL Client is used to make REST requests specifically when interacting with LNURL. If you want to use your own, you can it provide by implementing the REST Service interface. ## With Account Number API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.SdkBuilder.html#method.with_account_number The SDK derives all wallet keys from the seed at the derivation path `m/8797555'/'`. By default the account number is 0 on Regtest and 1 on all other networks. Set a different account number to derive an independent wallet from the same seed: ### Rust ```rust let account_number = 21; builder.with_account_number(account_number) ``` ### Swift ```swift let accountNumber = UInt32(21) await builder.withAccountNumber(accountNumber: accountNumber) ``` ### Kotlin ```kotlin val accountNumber = 21u builder.withAccountNumber(accountNumber) ``` ### C# ```csharp var accountNumber = 21u; await builder.WithAccountNumber(accountNumber); ``` ### Javascript (Wasm) ```typescript builder = builder.withAccountNumber(21) ``` ### React Native ```typescript await builder.withAccountNumber(21) ``` ### Flutter ```dart var accountNumber = 21; builder.withAccountNumber(accountNumber: accountNumber); ``` ### Python ```python account_number = 21 await builder.with_account_number(account_number=account_number) ``` ### Go ```go accountNumber := uint32(21) builder.WithAccountNumber(accountNumber) ``` ## With Payment Observer API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.SdkBuilder.html#method.with_payment_observer By implementing the Payment Observer interface you can be notified before a payment is sent. It includes information about the provisional payment including the payment ID, amount to be sent (in satoshis or token base units) and payment details based on the payment method. **Note:** Flutter currently does not support this. ### Rust ```rust pub(crate) struct ExamplePaymentObserver {} #[async_trait] impl PaymentObserver for ExamplePaymentObserver { async fn before_send( &self, payments: Vec, ) -> Result<(), PaymentObserverError> { for payment in payments { info!( "About to send payment: {:?} of amount {:?}", payment.payment_id, payment.amount ); } Ok(()) } async fn after_send(&self, updates: Vec) -> Result<(), PaymentObserverError> { for update in updates { info!( "Token tx broadcast: {} -> {}", update.provisional_payment_id, update.final_payment_id ); } Ok(()) } } pub(crate) fn with_payment_observer(builder: SdkBuilder) -> SdkBuilder { let observer = ExamplePaymentObserver {}; builder.with_payment_observer(Arc::new(observer)) } ``` ### Swift ```swift class ExamplePaymentObserver: PaymentObserver { func beforeSend(payments: [ProvisionalPayment]) async { for payment in payments { print("About to send payment: \(payment.paymentId) of amount \(payment.amount)") } } func afterSend(updates: [PaymentIdUpdate]) async { for update in updates { print("Token tx broadcast: \(update.provisionalPaymentId) -> \(update.finalPaymentId)") } } } func withPaymentObserver(builder: SdkBuilder) async { let paymentObserver = ExamplePaymentObserver() await builder.withPaymentObserver(paymentObserver: paymentObserver) } ``` ### Kotlin ```kotlin class ExamplePaymentObserver : PaymentObserver { override suspend fun beforeSend(payments: List) { for (payment in payments) { // Log.v("PaymentObserver", "About to send payment: // ${payment.paymentId} of amount ${payment.amount}") } } override suspend fun afterSend(updates: List) { for (update in updates) { // Log.v("PaymentObserver", "Token tx broadcast: // ${update.provisionalPaymentId} -> ${update.finalPaymentId}") } } } suspend fun withPaymentObserver(builder: SdkBuilder) { val paymentObserver = ExamplePaymentObserver() builder.withPaymentObserver(paymentObserver) } ``` ### C# ```csharp class ExamplePaymentObserver : PaymentObserver { public async Task BeforeSend(ProvisionalPayment[] payments) { foreach (var payment in payments) { Console.WriteLine($"About to send payment {payment.paymentId} " + $"of amount {payment.amount}"); } } public async Task AfterSend(PaymentIdUpdate[] updates) { foreach (var update in updates) { Console.WriteLine($"Token tx broadcast: {update.provisionalPaymentId} -> " + $"{update.finalPaymentId}"); } } } async Task WithPaymentObserver(SdkBuilder builder) { var paymentObserver = new ExamplePaymentObserver(); await builder.WithPaymentObserver(paymentObserver); } ``` ### Javascript (Wasm) ```typescript class ExamplePaymentObserver { beforeSend = async (payments: ProvisionalPayment[]) => { for (const payment of payments) { console.log(`About to send payment: ${payment.paymentId} of amount ${payment.amount}`) } } afterSend = async (updates: PaymentIdUpdate[]) => { for (const update of updates) { console.log(`Token tx broadcast: ${update.provisionalPaymentId} -> ${update.finalPaymentId}`) } } } const exampleWithPaymentObserver = (builder: SdkBuilder): SdkBuilder => { const paymentObserver = new ExamplePaymentObserver() return builder.withPaymentObserver(paymentObserver) } ``` ### React Native ```typescript class ExamplePaymentObserver { beforeSend = async (payments: ProvisionalPayment[]) => { for (const payment of payments) { console.log(`About to send payment: ${payment.paymentId} of amount ${payment.amount}`) } } afterSend = async (updates: PaymentIdUpdate[]) => { for (const update of updates) { console.log(`Token tx broadcast: ${update.provisionalPaymentId} -> ${update.finalPaymentId}`) } } } const exampleWithPaymentObserver = async (builder: SdkBuilder) => { const paymentObserver = new ExamplePaymentObserver() await builder.withPaymentObserver(paymentObserver) } ``` ### Flutter ```dart ``` ### Python ```python class ExamplePaymentObserver(PaymentObserver): async def before_send(self, payments: typing.List[ProvisionalPayment]): for payment in payments: logging.debug(f"About to send payment {payment.payment_id} of amount {payment.amount}") async def after_send(self, updates: typing.List[PaymentIdUpdate]): for update in updates: logging.debug( f"Token tx broadcast: {update.provisional_payment_id} -> {update.final_payment_id}" ) async def with_payment_observer(builder: SdkBuilder): payment_observer = ExamplePaymentObserver() await builder.with_payment_observer(payment_observer=payment_observer) ``` ### Go ```go type ExamplePaymentObserver struct{} func (ExamplePaymentObserver) BeforeSend(payments []breez_sdk_spark.ProvisionalPayment) error { for _, payment := range payments { log.Printf("About to send payment: %v of amount %v", payment.PaymentId, payment.Amount) } return nil } func (ExamplePaymentObserver) AfterSend(updates []breez_sdk_spark.PaymentIdUpdate) error { for _, update := range updates { log.Printf("Token tx broadcast: %v -> %v", update.ProvisionalPaymentId, update.FinalPaymentId) } return nil } func WithPaymentObserver(builder *breez_sdk_spark.SdkBuilder) { observer := ExamplePaymentObserver{} builder.WithPaymentObserver(observer) } ``` ## With Session Store API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.SdkBuilder.html#method.with_session_store The SDK caches the auth tokens it obtains from the Spark operators and the SSP in a session store, keyed by each service's identity. By default the store is provided by the storage backend (a `brz_`-prefixed table on the PostgreSQL/MySQL backends, an in-memory store otherwise), and tokens are stored as-is. Use `with_session_store` to provide your own `SessionStore`. This can be a completely different persistence layer, or a decorator that wraps the backend's own store to transform tokens on read and write while keeping its persistence: fetch the backend's store with `default_session_store`, then intercept `get_session` and `set_session`. At-rest encryption is one such transform (the SDK does not encrypt tokens itself), shown below: encrypt the token in `set_session` and decrypt it in `get_session`. ### Rust ```rust pub(crate) struct EncryptingSessionStore { inner: Arc, } #[async_trait] impl SessionStore for EncryptingSessionStore { async fn get_session( &self, service_identity_key: PublicKey, ) -> Result { let session = self.inner.get_session(service_identity_key).await?; // Decrypt session.token here before returning it. Ok(session) } async fn set_session( &self, service_identity_key: PublicKey, session: Session, ) -> Result<(), SessionStoreError> { // Encrypt session.token here before persisting it. self.inner.set_session(service_identity_key, session).await } } // `identity` is the wallet identity public key bytes, used to scope the store. pub(crate) async fn with_session_store( config: Config, seed: Seed, identity: Vec, ) -> Result { // Reuse one storage backend for both the SDK storage and the session store. let backend = default_storage("./.data".to_string()); // Get the session store the backend provides, then wrap it to add encryption. let inner = default_session_store(backend.clone(), config.network, identity).await?; let session_store = Arc::new(EncryptingSessionStore { inner }); Ok(SdkBuilder::new(config, seed) .with_storage_backend(backend) .with_session_store(session_store)) } ``` ### Swift ```swift class EncryptingSessionStore: SessionStore { let inner: SessionStore init(inner: SessionStore) { self.inner = inner } func getSession(serviceIdentityKey: String) async throws -> Session { let session = try await inner.getSession(serviceIdentityKey: serviceIdentityKey) // Decrypt session.token here before returning it. return session } func setSession(serviceIdentityKey: String, session: Session) async throws { // Encrypt session.token here before persisting it. try await inner.setSession(serviceIdentityKey: serviceIdentityKey, session: session) } } // `identity` is the wallet identity public key bytes, used to scope the store. func withSessionStore(config: Config, seed: Seed, identity: Data) async throws -> SdkBuilder { // Reuse one storage backend for both the SDK storage and the session store. let backend = defaultStorage(storageDir: "./.data") // Get the session store the backend provides, then wrap it to add encryption. let inner = try await defaultSessionStore( backend: backend, network: config.network, identity: identity ) let sessionStore = EncryptingSessionStore(inner: inner) let builder = SdkBuilder(config: config, seed: seed) await builder.withStorageBackend(storage: backend) await builder.withSessionStore(sessionStore: sessionStore) return builder } ``` ### Kotlin ```kotlin class EncryptingSessionStore( private val inner: SessionStore, ) : SessionStore { override suspend fun getSession(serviceIdentityKey: String): Session { val session = inner.getSession(serviceIdentityKey) // Decrypt session.token here before returning it. return session } override suspend fun setSession(serviceIdentityKey: String, session: Session) { // Encrypt session.token here before persisting it. inner.setSession(serviceIdentityKey, session) } } // `identity` is the wallet identity public key bytes, used to scope the store. suspend fun withSessionStore( config: breez_sdk_spark.Config, seed: Seed, identity: ByteArray, ): SdkBuilder { // Reuse one storage backend for both the SDK storage and the session store. val backend = defaultStorage("./.data") // Get the session store the backend provides, then wrap it to add encryption. val inner = defaultSessionStore(backend, config.network, identity) val sessionStore = EncryptingSessionStore(inner) val builder = SdkBuilder(config, seed) builder.withStorageBackend(backend) builder.withSessionStore(sessionStore) return builder } ``` ### C# ```csharp class EncryptingSessionStore : SessionStore { private readonly SessionStore inner; public EncryptingSessionStore(SessionStore inner) { this.inner = inner; } public async Task GetSession(string serviceIdentityKey) { var session = await inner.GetSession(serviceIdentityKey); // Decrypt session.token here before returning it. return session; } public async Task SetSession(string serviceIdentityKey, Session session) { // Encrypt session.token here before persisting it. await inner.SetSession(serviceIdentityKey, session); } } // `identity` is the wallet identity public key bytes, used to scope the store. async Task WithSessionStore(Config config, Seed seed, byte[] identity) { // Reuse one storage backend for both the SDK storage and the session store. var backend = BreezSdkSparkMethods.DefaultStorage(storageDir: "./.data"); // Get the session store the backend provides, then wrap it to add encryption. var inner = await BreezSdkSparkMethods.DefaultSessionStore( backend: backend, network: config.network, identity: identity ); var sessionStore = new EncryptingSessionStore(inner); var builder = new SdkBuilder(config: config, seed: seed); await builder.WithStorageBackend(storage: backend); await builder.WithSessionStore(sessionStore: sessionStore); return builder; } ``` ### Javascript (Wasm) ```typescript class EncryptingSessionStore implements SessionStore { constructor (private readonly inner: DefaultSessionStore) {} getSession = async (serviceIdentityKey: string): Promise => { const session = await this.inner.getSession(serviceIdentityKey) // Decrypt session.token here before returning it. return session } setSession = async (serviceIdentityKey: string, session: Session): Promise => { // Encrypt session.token here before persisting it. await this.inner.setSession(serviceIdentityKey, session) } } const exampleWithSessionStore = async (identity: string): Promise => { // Construct the seed using a mnemonic, entropy or passkey const mnemonic = '' const seed: Seed = { type: 'mnemonic', mnemonic, passphrase: undefined } // Create the default config const config = defaultConfig('mainnet') config.apiKey = '' // Get the backend's own session store, then wrap it to encrypt at rest. // identity is the wallet identity public key, hex. const storageConfig = defaultStorage('./.data') const inner = await defaultSessionStore(storageConfig, 'mainnet', identity) const sessionStore = new EncryptingSessionStore(inner) let builder = SdkBuilder.new(config, seed) builder = builder.withStorageBackend(storageConfig) builder = builder.withSessionStore(sessionStore) return builder } ``` ### React Native ```typescript class EncryptingSessionStore implements SessionStore { constructor (private readonly inner: SessionStore) {} getSession = async (serviceIdentityKey: string): Promise => { const session = await this.inner.getSession(serviceIdentityKey) // Decrypt session.token here before returning it. return session } setSession = async (serviceIdentityKey: string, session: Session): Promise => { // Encrypt session.token here before persisting it. await this.inner.setSession(serviceIdentityKey, session) } } // `identity` is the wallet identity public key bytes, used to scope the store. const exampleWithSessionStore = async (identity: ArrayBuffer): Promise => { // Construct the seed using a mnemonic, entropy or passkey const mnemonic = '' const seed = new Seed.Mnemonic({ mnemonic, passphrase: undefined }) // Create the default config const config = defaultConfig(Network.Mainnet) config.apiKey = '' // Reuse one storage backend for both the SDK storage and the session store. const backend = defaultStorage('./.data') // Get the session store the backend provides, then wrap it to add encryption. const inner = await defaultSessionStore(backend, config.network, identity) const sessionStore = new EncryptingSessionStore(inner) const builder = new SdkBuilder(config, seed) await builder.withStorageBackend(backend) await builder.withSessionStore(sessionStore) return builder } ``` ### Flutter ```dart ``` ### Python ```python class EncryptingSessionStore(SessionStore): def __init__(self, inner: SessionStore): self.inner = inner async def get_session(self, service_identity_key: str) -> Session: session = await self.inner.get_session(service_identity_key) # Decrypt session.token here before returning it. return session async def set_session(self, service_identity_key: str, session: Session) -> None: # Encrypt session.token here before persisting it. await self.inner.set_session(service_identity_key, session) # `identity` is the wallet identity public key bytes, used to scope the store. async def with_session_store(config: Config, seed: Seed, identity: bytes) -> SdkBuilder: # Reuse one storage backend for both the SDK storage and the session store. backend = default_storage(storage_dir="./.data") # Get the session store the backend provides, then wrap it to add encryption. inner = await default_session_store( backend=backend, network=config.network, identity=identity, ) session_store = EncryptingSessionStore(inner) builder = SdkBuilder(config=config, seed=seed) await builder.with_storage_backend(storage=backend) await builder.with_session_store(session_store=session_store) return builder ``` ### Go ```go type EncryptingSessionStore struct { inner breez_sdk_spark.SessionStore } func (s EncryptingSessionStore) GetSession( serviceIdentityKey string, ) (breez_sdk_spark.Session, error) { session, err := s.inner.GetSession(serviceIdentityKey) if err != nil { return breez_sdk_spark.Session{}, err } // Decrypt session.Token here before returning it. return session, nil } func (s EncryptingSessionStore) SetSession( serviceIdentityKey string, session breez_sdk_spark.Session, ) error { // Encrypt session.Token here before persisting it. return s.inner.SetSession(serviceIdentityKey, session) } // `identity` is the wallet identity public key bytes, used to scope the store. func WithSessionStore( config breez_sdk_spark.Config, seed breez_sdk_spark.Seed, identity []byte, ) (*breez_sdk_spark.SdkBuilder, error) { // Reuse one storage backend for both the SDK storage and the session store. backend := breez_sdk_spark.DefaultStorage("./.data") // Get the session store the backend provides, then wrap it to add encryption. inner, err := breez_sdk_spark.DefaultSessionStore(backend, config.Network, identity) if err != nil { return nil, err } sessionStore := EncryptingSessionStore{inner: inner} builder := breez_sdk_spark.NewSdkBuilder(config, seed) builder.WithStorageBackend(backend) builder.WithSessionStore(sessionStore) return builder, nil } ``` **Developer note** When wrapping the backend's store, pass the same storage backend to both `with_storage_backend` and `default_session_store` so the session store shares the SDK's persistence. On the WASM binding `default_session_store` takes the storage config and the wallet identity public key (hex) instead of a backend. **Note:** Not supported in Flutter. ## With Shared SDK Context API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.SdkContext.html An SDK Context bundles every process-shareable resource: the HTTP client (used for SSP GraphQL, chain service and LNURL), the gRPC channels to the Spark operators, the gRPC client to the Breez backend, and — optionally — a PostgreSQL or MySQL connection pool. By default each SDK builds its own. Server processes hosting many wallets at once can construct one SDK Context and pass it to every `SdkBuilder` so they reuse the same pooled clients instead of each opening fresh ones. Construct one via `new_shared_sdk_context` and pass it to each `SdkBuilder` via `with_shared_context`. Connections close when the last reference to the SDK Context is dropped; calling `disconnect` on an SDK instance does not affect them. The `connections_per_operator` setting on `SdkContextConfig` controls how many gRPC connections the context opens to each Spark operator: - `None` — one connection per operator, multiplexed across every SDK sharing this context. The right choice for almost every deployment. - `Some(n)` — opens `n` connections per operator and balances requests across them. Worth setting only if the single shared connection has become a bottleneck — for example, latency that climbs with throughput, or operators deployed behind an L7 load balancer where you want client-side fan-out across backend instances. To route a context's pooled connections through a SOCKS5 proxy, set `proxy` on `SdkContextConfig` as well as on each SDK's `Config`. See [SOCKS5 proxy](/guide/proxy.md). **Developer note** All SDK instances sharing an SDK Context must be configured for the same network and operator pool, and must agree on `proxy`. The user agent of the first SDK to construct the context is reused for all subsequent instances. ### Browser The SDK Context's gRPC channel pooling is not effective in the browser. Browsers maintain a single HTTP/2 connection per origin and multiplex everything over it; the SDK cannot create or share more. ### Node.js Node's global `fetch` (undici) negotiates HTTP/2 with the Spark operators automatically and opens additional connections per origin as needed, so most deployments need no tuning. If you do want to cap or expand the per-origin pool, configure undici globally before initialising the SDK: ```js import { Agent, setGlobalDispatcher } from 'undici' setGlobalDispatcher(new Agent({ connections: 8 })) ``` This affects every `fetch` in the process, including the SDK's gRPC-web traffic. --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Getting the SDK info](/guide/get_info.md)** # Getting the SDK info API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.get_info Once connected, you can retrieve the current state of the SDK at any time using `get_info`. This returns: - **Spark identity public key** - The wallet's unique identity on the Spark network as a hex string - **Bitcoin balance** - The balance in satoshis - **Token balances** - Balances of any tokens held in the wallet ## Rust ```rust let info = sdk .get_info(GetInfoRequest { // ensure_synced: true will ensure the SDK is synced with the Spark network // before returning the balance ensure_synced: Some(false), }) .await?; let identity_pubkey = &info.identity_pubkey; let balance_sats = info.balance_sats; ``` ## Swift ```swift // ensureSynced: true will ensure the SDK is synced with the Spark network // before returning the balance let info = try await sdk.getInfo( request: GetInfoRequest( ensureSynced: false )) let identityPubkey = info.identityPubkey let balanceSats = info.balanceSats ``` ## Kotlin ```kotlin try { // ensureSynced: true will ensure the SDK is synced with the Spark network // before returning the balance val info = sdk.getInfo(GetInfoRequest(false)) val identityPubkey = info.identityPubkey val balanceSats = info.balanceSats } catch (e: Exception) { // handle error } ``` ## C# ```csharp // ensureSynced: true will ensure the SDK is synced with the Spark network // before returning the balance var info = await sdk.GetInfo(request: new GetInfoRequest(ensureSynced: false)); var identityPubkey = info.identityPubkey; var balanceSats = info.balanceSats; ``` ## Javascript (Wasm) ```typescript const info = await sdk.getInfo({ // ensureSynced: true will ensure the SDK is synced with the Spark network // before returning the balance ensureSynced: false }) const identityPubkey = info.identityPubkey const balanceSats = info.balanceSats ``` ## React Native ```typescript // ensureSynced: true will ensure the SDK is synced with the Spark network // before returning the balance const info = await sdk.getInfo({ ensureSynced: false }) const identityPubkey = info.identityPubkey const balanceSats = info.balanceSats ``` ## Flutter ```dart // ensureSynced: true will ensure the SDK is synced with the Spark network // before returning the balance final info = await sdk.getInfo(request: GetInfoRequest(ensureSynced: false)); final identityPubkey = info.identityPubkey; final balanceSats = info.balanceSats; ``` ## Python ```python try: # ensure_synced: True will ensure the SDK is synced with the Spark network # before returning the balance info = await sdk.get_info(request=GetInfoRequest(ensure_synced=False)) identity_pubkey = info.identity_pubkey balance_sats = info.balance_sats except Exception as error: logging.error(error) raise ``` ## Go ```go ensureSynced := false info, err := sdk.GetInfo(breez_sdk_spark.GetInfoRequest{ // EnsureSynced: true will ensure the SDK is synced with the Spark network // before returning the balance EnsureSynced: &ensureSynced, }) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } identityPubkey := info.IdentityPubkey balanceSats := info.BalanceSats log.Printf("Identity pubkey: %v, Balance: %v sats", identityPubkey, balanceSats) ``` ## Fetching the balance The SDK keeps a **cached balance** in local storage and `get_info` reads from this cache for a low-latency response. The cache is refreshed automatically by the SDK's background sync. The recommended pattern is: 1. Call `get_info` with `ensure_synced` = **false** whenever you need to render the balance. 2. Subscribe to events and call `get_info` again on each `SdkEvent::Synced` event to fetch the latest balance. See [Listening to events](/guide/events.md). | Event | Description | UX Suggestion | | ----- | ----------- | ------------- | | `SdkEvent::Synced` | The SDK has synced with the network in the background. | Call `get_info` to refresh the displayed balance, and refresh the payments list. See [listing payments](/guide/list_payments.md). | **Developer note** `ensure_synced` = **true** blocks until the SDK's **initial** sync after `connect` completes. This is useful for short-lived scripts that connect, read the balance once, and disconnect. It is **not** a "force a fresh sync now" call. In long-running applications, prefer `ensure_synced` = **false** combined with the `SdkEvent::Synced` event listener pattern above. ## Server mode When the SDK is built with [Server mode](/guide/server_mode.md), `get_info` reads the balance live from the spark wallet's local tree store rather than from the background-maintained cache. As a result: - `ensure_synced` = **true** is rejected with an invalid-input error. The SDK has no initial-sync watcher to await; call `sync_wallet` explicitly if you need to refresh state first. - The returned balance reflects whatever is currently in the local tree store. If you need the freshest possible balance after an external state change (an incoming Spark transfer claimed elsewhere, an on-chain deposit confirmed, etc.), call `sync_wallet` first. --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Listening to events](/guide/events.md)** # Listening to events The SDK emits several events to provide the application with an up-to-date state of the SDK or ongoing payments. ## Event reference | Event | Payload | What it means and what to do | | --- | --- | --- | | `SdkEvent::Synced` | none | The wallet finished syncing with the network. Refresh the balance and the payment list. See [getting the SDK info](/guide/get_info.md). | | `SdkEvent::PaymentSucceeded` | `Payment` | A payment completed. The SDK refreshes its cached balance before emitting this, so `get_info` returns the new value. | | `SdkEvent::PaymentPending` | `Payment` | A payment is in flight. The same payment is emitted again as succeeded or failed once it settles. | | `SdkEvent::PaymentFailed` | `Payment` | A payment failed. Its `details` carry the method-specific context to show the user. | | `SdkEvent::NewDeposits` | `DepositInfo` list | On-chain deposits were detected. Only deposits whose `is_mature` is true can be claimed, so show the rest as pending. | | `SdkEvent::ClaimedDeposits` | `DepositInfo` list | Deposits were claimed into the wallet. The matching payment is emitted separately as `SdkEvent::PaymentSucceeded`. | | `SdkEvent::UnclaimedDeposits` | `DepositInfo` list | The SDK could not claim these. Read `claim_error` for the reason, then claim manually or refund. See [claiming on-chain deposits](/guide/onchain_claims.md). | | `SdkEvent::AutoOptimization` | `AutoOptimizationEvent` | Progress of the background leaf optimizer. Manual `optimize_leaves` calls do not emit this. See [custom leaf optimization](/guide/optimize.md). | | `SdkEvent::LightningAddressChanged` | `LightningAddressInfo`, unset when the address was deleted | The Lightning address changed on another device. See [receiving payments using LNURL-Pay](/guide/receive_lnurl_pay.md). | | `SdkEvent::UnilateralExitStateChanged` | none | An exit state exported earlier is now out of date. Export it again. See [unilateral exit](/guide/unilateral_exit.md). | The fields of `Payment` are described in [listing payments](/guide/list_payments.md). For the order in which these events arrive during a receive, see [receiving payments](/guide/receive_payment.md). ### Deposit fields The three deposit events each carry a list of `DepositInfo`, whose fields determine what to do next. | Field | Meaning | | --- | --- | | `txid`, `vout` | The on-chain output the deposit came from. | | `amount_sats` | Deposit value in satoshis. | | `is_mature` | Whether the deposit has enough confirmations to be claimed. | | `claim_error` | Why the last claim attempt failed. Set on `SdkEvent::UnclaimedDeposits`. | | `refund_tx`, `refund_tx_id` | The refund transaction, once one has been created. | | `refund_state` | How far the refund has got towards the network. Read it through `list_unclaimed_deposits`: a refunded deposit no longer appears in these events. See [tracking a refund](/guide/onchain_claims.md#tracking-a-refund). | | `instant_claim_status` | State of an instant (0-conf) claim attempt. Unset when none was attempted. | ## Add event listener API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.add_event_listener ### Rust ```rust pub(crate) struct SdkEventListener {} #[async_trait::async_trait] impl EventListener for SdkEventListener { async fn on_event(&self, e: SdkEvent) { match e { SdkEvent::Synced => { // Data has been synchronized with the network. When this event is received, // it is recommended to refresh the payment list and wallet balance. } SdkEvent::NewDeposits { new_deposits } => { // Detected deposits, as DepositInfo. Only those with is_mature set // have enough confirmations to be claimed. Show the rest as pending. } SdkEvent::UnclaimedDeposits { unclaimed_deposits } => { // Deposits the SDK could not claim. Each claim_error says why, // most often the fee exceeded the configured maximum. } SdkEvent::ClaimedDeposits { claimed_deposits } => { // Deposits claimed into the wallet. An instant (0-conf) claim is // reported here on submission and settles shortly after. } SdkEvent::PaymentSucceeded { payment } => { // A payment completed. The cached balance is already refreshed, // so get_info returns the new value. } SdkEvent::PaymentPending { payment } => { // A payment is awaiting confirmation. It arrives again as // succeeded or failed once it settles. } SdkEvent::PaymentFailed { payment } => { // A payment failed. payment.details carries the method-specific // context to show the user. } SdkEvent::AutoOptimization { optimization_event } => { // Background optimizer progress: started, round completed, or a // terminal outcome. Manual optimize_leaves calls do not emit these. } SdkEvent::LightningAddressChanged { lightning_address } => { // The lightning address changed on another device. Unset when the // address was deleted. } SdkEvent::UnilateralExitStateChanged => { // The unilateral exit state changed, so a previously exported // one is now out of date. Export it again. } } } } pub(crate) async fn add_event_listener( sdk: &BreezSdk, listener: Box, ) -> Result { let listener_id = sdk.add_event_listener(listener).await; Ok(listener_id) } ``` ### Swift ```swift class SdkEventListener: EventListener { func onEvent(event: SdkEvent) async { switch event { case .synced: // Data has been synchronized with the network. When this event is received, // it is recommended to refresh the payment list and wallet balance. break case .newDeposits(let newDeposits): // Detected deposits, as DepositInfo. Only those with isMature set // have enough confirmations to be claimed. Show the rest as pending. let _ = newDeposits case .unclaimedDeposits(let unclaimedDeposits): // Deposits the SDK could not claim. Each claimError says why, // most often the fee exceeded the configured maximum. let _ = unclaimedDeposits case .claimedDeposits(let claimedDeposits): // Deposits claimed into the wallet. An instant (0-conf) claim is // reported here on submission and settles shortly after. let _ = claimedDeposits case .paymentSucceeded(let paymentSucceeded): // A payment completed. The cached balance is already refreshed, // so getInfo returns the new value. let _ = paymentSucceeded case .paymentPending(let paymentPending): // A payment is awaiting confirmation. It arrives again as // succeeded or failed once it settles. let _ = paymentPending case .paymentFailed(let paymentFailed): // A payment failed. payment.details carries the method-specific // context to show the user. let _ = paymentFailed case .autoOptimization(let optimizationEvent): // Background optimizer progress: started, round completed, or a // terminal outcome. Manual optimizeLeaves calls do not emit these. let _ = optimizationEvent case .lightningAddressChanged(let lightningAddress): // The lightning address changed on another device. Unset when the // address was deleted. let _ = lightningAddress case .unilateralExitStateChanged: // The unilateral exit state changed, so a previously exported // one is now out of date. Export it again. break default: // Handle any future event types break } } } func addEventListener(sdk: BreezSdk, listener: SdkEventListener) async -> String { let listenerId = await sdk.addEventListener(listener: listener) return listenerId } ``` ### Kotlin ```kotlin class SdkListener : EventListener { override suspend fun onEvent(e: SdkEvent) { when (e) { is SdkEvent.Synced -> { // Data has been synchronized with the network. When this event is received, // it is recommended to refresh the payment list and wallet balance. } is SdkEvent.NewDeposits -> { // Detected deposits, as DepositInfo. Only those with isMature set // have enough confirmations to be claimed. Show the rest as pending. val newDeposits = e.newDeposits } is SdkEvent.UnclaimedDeposits -> { // Deposits the SDK could not claim. Each claimError says why, // most often the fee exceeded the configured maximum. val unclaimedDeposits = e.unclaimedDeposits } is SdkEvent.ClaimedDeposits -> { // Deposits claimed into the wallet. An instant (0-conf) claim is // reported here on submission and settles shortly after. val claimedDeposits = e.claimedDeposits } is SdkEvent.PaymentSucceeded -> { // A payment completed. The cached balance is already refreshed, // so getInfo returns the new value. val payment = e.payment } is SdkEvent.PaymentPending -> { // A payment is awaiting confirmation. It arrives again as // succeeded or failed once it settles. val pendingPayment = e.payment } is SdkEvent.PaymentFailed -> { // A payment failed. payment.details carries the method-specific // context to show the user. val failedPayment = e.payment } is SdkEvent.AutoOptimization -> { // Background optimizer progress: started, round completed, or a // terminal outcome. Manual optimizeLeaves calls do not emit these. val optimizationEvent = e.optimizationEvent } is SdkEvent.LightningAddressChanged -> { // The lightning address changed on another device. Unset when the // address was deleted. val lightningAddress = e.lightningAddress } is SdkEvent.UnilateralExitStateChanged -> { // The unilateral exit state changed, so a previously exported // one is now out of date. Export it again. } else -> { // Handle any future event types } } } } suspend fun addEventListener(sdk: BreezSdk, listener: SdkListener): String? { try { val listenerId = sdk.addEventListener(listener) return listenerId } catch (e: Exception) { // handle error return null } } ``` ### C# ```csharp class SdkListener : EventListener { public async Task OnEvent(SdkEvent sdkEvent) { switch (sdkEvent) { case SdkEvent.Synced syncedEvent: // Data has been synchronized with the network. When this event is received, // it is recommended to refresh the payment list and wallet balance. break; case SdkEvent.NewDeposits newDepositsEvent: // Detected deposits, as DepositInfo. Only those with IsMature set // have enough confirmations to be claimed. Show the rest as pending. var newDeposits = newDepositsEvent.newDeposits; break; case SdkEvent.UnclaimedDeposits unclaimedDepositsEvent: // Deposits the SDK could not claim. Each ClaimError says why, // most often the fee exceeded the configured maximum. var unclaimedDeposits = unclaimedDepositsEvent.unclaimedDeposits; break; case SdkEvent.ClaimedDeposits claimedDepositsEvent: // Deposits claimed into the wallet. An instant (0-conf) claim is // reported here on submission and settles shortly after. var claimedDeposits = claimedDepositsEvent.claimedDeposits; break; case SdkEvent.PaymentSucceeded paymentSucceededEvent: // A payment completed. The cached balance is already refreshed, // so GetInfo returns the new value. var payment = paymentSucceededEvent.payment; break; case SdkEvent.PaymentPending paymentPendingEvent: // A payment is awaiting confirmation. It arrives again as // succeeded or failed once it settles. var pendingPayment = paymentPendingEvent.payment; break; case SdkEvent.PaymentFailed paymentFailedEvent: // A payment failed. payment.Details carries the method-specific // context to show the user. var failedPayment = paymentFailedEvent.payment; break; case SdkEvent.AutoOptimization optimizationEvent: // Background optimizer progress: started, round completed, or a // terminal outcome. Manual OptimizeLeaves calls do not emit these. var optimization = optimizationEvent.optimizationEvent; break; case SdkEvent.LightningAddressChanged lightningAddressChangedEvent: // The lightning address changed on another device. Unset when the // address was deleted. var lightningAddress = lightningAddressChangedEvent.lightningAddress; break; case SdkEvent.UnilateralExitStateChanged unilateralExitStateChangedEvent: // The unilateral exit state changed, so a previously exported // one is now out of date. Export it again. break; default: // Handle any future event types break; } } } async Task AddEventListener(BreezSdk sdk, SdkListener listener) { var listenerId = await sdk.AddEventListener(listener: listener); return listenerId; } ``` ### Javascript (Wasm) ```typescript class JsEventListener { onEvent = async (event: SdkEvent) => { switch (event.type) { case 'synced': { // Data has been synchronized with the network. When this event is received, // it is recommended to refresh the payment list and wallet balance. break } case 'newDeposits': { // Detected deposits, as DepositInfo. Only those with isMature set // have enough confirmations to be claimed. Show the rest as pending. const newDeposits = event.newDeposits break } case 'unclaimedDeposits': { // Deposits the SDK could not claim. Each claimError says why, // most often the fee exceeded the configured maximum. const unclaimedDeposits = event.unclaimedDeposits break } case 'claimedDeposits': { // Deposits claimed into the wallet. An instant (0-conf) claim is // reported here on submission and settles shortly after. const claimedDeposits = event.claimedDeposits break } case 'paymentSucceeded': { // A payment completed. The cached balance is already refreshed, // so getInfo returns the new value. const payment = event.payment break } case 'paymentPending': { // A payment is awaiting confirmation. It arrives again as // succeeded or failed once it settles. const pendingPayment = event.payment break } case 'paymentFailed': { // A payment failed. payment.details carries the method-specific // context to show the user. const failedPayment = event.payment break } case 'autoOptimization': { // Background optimizer progress: started, round completed, or a // terminal outcome. Manual optimizeLeaves calls do not emit these. const optimizationEvent = event.optimizationEvent break } case 'lightningAddressChanged': { // The lightning address changed on another device. Unset when the // address was deleted. const lightningAddress = event.lightningAddress break } case 'unilateralExitStateChanged': { // The unilateral exit state changed, so a previously exported // one is now out of date. Export it again. break } default: { // Handle any future event types break } } } } const eventListener = new JsEventListener() const listenerId = await sdk.addEventListener(eventListener) ``` ### React Native ```typescript class JsEventListener { onEvent = async (event: SdkEvent) => { if (event.tag === SdkEvent_Tags.Synced) { // Data has been synchronized with the network. When this event is received, // it is recommended to refresh the payment list and wallet balance. } else if (event.tag === SdkEvent_Tags.NewDeposits) { // Detected deposits, as DepositInfo. Only those with isMature set // have enough confirmations to be claimed. Show the rest as pending. const newDeposits = event.inner.newDeposits } else if (event.tag === SdkEvent_Tags.UnclaimedDeposits) { // Deposits the SDK could not claim. Each claimError says why, // most often the fee exceeded the configured maximum. const unclaimedDeposits = event.inner.unclaimedDeposits } else if (event.tag === SdkEvent_Tags.ClaimedDeposits) { // Deposits claimed into the wallet. An instant (0-conf) claim is // reported here on submission and settles shortly after. const claimedDeposits = event.inner.claimedDeposits } else if (event.tag === SdkEvent_Tags.PaymentSucceeded) { // A payment completed. The cached balance is already refreshed, // so getInfo returns the new value. const payment = event.inner.payment } else if (event.tag === SdkEvent_Tags.PaymentPending) { // A payment is awaiting confirmation. It arrives again as // succeeded or failed once it settles. const pendingPayment = event.inner.payment } else if (event.tag === SdkEvent_Tags.PaymentFailed) { // A payment failed. payment.details carries the method-specific // context to show the user. const failedPayment = event.inner.payment } else if (event.tag === SdkEvent_Tags.AutoOptimization) { // Background optimizer progress: started, round completed, or a // terminal outcome. Manual optimizeLeaves calls do not emit these. const optimizationEvent = event.inner.optimizationEvent } else if (event.tag === SdkEvent_Tags.LightningAddressChanged) { // The lightning address changed on another device. Unset when the // address was deleted. const lightningAddress = event.inner.lightningAddress } else if (event.tag === SdkEvent_Tags.UnilateralExitStateChanged) { // The unilateral exit state changed, so a previously exported // one is now out of date. Export it again. } else { // Handle any future event types } } } const eventListener = new JsEventListener() const listenerId = await sdk.addEventListener(eventListener) ``` ### Flutter ```dart StreamSubscription? _eventSubscription; Stream? _eventStream; // Initializes SDK event stream. // // Call once on your Dart entrypoint file, e.g.; `lib/main.dart` // or singleton SDK service. It is recommended to use a single instance // of the SDK across your Flutter app. void initializeEventsStream(BreezSdk sdk) { _eventStream ??= sdk.addEventListener().asBroadcastStream(); } final _eventStreamController = StreamController.broadcast(); Stream get eventStream => _eventStreamController.stream; // Subscribe to the event stream void subscribeToEventStream() { _eventSubscription = _eventStream?.listen((sdkEvent) { switch (sdkEvent) { case SdkEvent_Synced(): // Data has been synchronized with the network. When this event is received, // it is recommended to refresh the payment list and wallet balance. break; case SdkEvent_NewDeposits(:final newDeposits): // Detected deposits, as DepositInfo. Only those with isMature set // have enough confirmations to be claimed. Show the rest as pending. final _ = newDeposits; break; case SdkEvent_UnclaimedDeposits(:final unclaimedDeposits): // Deposits the SDK could not claim. Each claimError says why, // most often the fee exceeded the configured maximum. final _ = unclaimedDeposits; break; case SdkEvent_ClaimedDeposits(:final claimedDeposits): // Deposits claimed into the wallet. An instant (0-conf) claim is // reported here on submission and settles shortly after. final _ = claimedDeposits; break; case SdkEvent_PaymentSucceeded(:final payment): // A payment completed. The cached balance is already refreshed, // so getInfo returns the new value. final _ = payment; break; case SdkEvent_PaymentPending(:final payment): // A payment is awaiting confirmation. It arrives again as // succeeded or failed once it settles. final _ = payment; break; case SdkEvent_PaymentFailed(:final payment): // A payment failed. payment.details carries the method-specific // context to show the user. final _ = payment; break; case SdkEvent_AutoOptimization(:final optimizationEvent): // Background optimizer progress: started, round completed, or a // terminal outcome. Manual optimizeLeaves calls do not emit these. final _ = optimizationEvent; break; case SdkEvent_LightningAddressChanged(:final lightningAddress): // The lightning address changed on another device. Unset when the // address was deleted. final _ = lightningAddress; break; case SdkEvent_UnilateralExitStateChanged(): // The unilateral exit state changed, so a previously exported // one is now out of date. Export it again. break; } _eventStreamController.add(sdkEvent); }, onError: (e) { _eventStreamController.addError(e); }); } ``` ### Python ```python class SdkListener(EventListener): async def on_event(self, event: SdkEvent): if isinstance(event, SdkEvent.SYNCED): # Data has been synchronized with the network. When this event is received, # it is recommended to refresh the payment list and wallet balance. pass elif isinstance(event, SdkEvent.NEW_DEPOSITS): # Detected deposits, as DepositInfo. Only those with is_mature set # have enough confirmations to be claimed. Show the rest as pending. new_deposits = event.new_deposits elif isinstance(event, SdkEvent.UNCLAIMED_DEPOSITS): # Deposits the SDK could not claim. Each claim_error says why, # most often the fee exceeded the configured maximum. unclaimed_deposits = event.unclaimed_deposits elif isinstance(event, SdkEvent.CLAIMED_DEPOSITS): # Deposits claimed into the wallet. An instant (0-conf) claim is # reported here on submission and settles shortly after. claimed_deposits = event.claimed_deposits elif isinstance(event, SdkEvent.PAYMENT_SUCCEEDED): # A payment completed. The cached balance is already refreshed, # so get_info returns the new value. payment = event.payment elif isinstance(event, SdkEvent.PAYMENT_PENDING): # A payment is awaiting confirmation. It arrives again as # succeeded or failed once it settles. pending_payment = event.payment elif isinstance(event, SdkEvent.PAYMENT_FAILED): # A payment failed. payment.details carries the method-specific # context to show the user. failed_payment = event.payment elif isinstance(event, SdkEvent.AUTO_OPTIMIZATION): # Background optimizer progress: started, round completed, or a # terminal outcome. Manual optimize_leaves calls do not emit these. optimization_event = event.optimization_event elif isinstance(event, SdkEvent.LIGHTNING_ADDRESS_CHANGED): # The lightning address changed on another device. Unset when the # address was deleted. lightning_address = event.lightning_address elif isinstance(event, SdkEvent.UNILATERAL_EXIT_STATE_CHANGED): # The unilateral exit state changed, so a previously exported # one is now out of date. Export it again. pass else: # Handle any future event types pass async def add_event_listener(sdk: BreezSdk, listener: SdkListener): try: listener_id = await sdk.add_event_listener(listener=listener) return listener_id except Exception as error: logging.error(error) raise ``` ### Go ```go type SdkListener struct{} func (SdkListener) OnEvent(e breez_sdk_spark.SdkEvent) { switch event := e.(type) { case breez_sdk_spark.SdkEventSynced: // Data has been synchronized with the network. When this event is received, // it is recommended to refresh the payment list and wallet balance. case breez_sdk_spark.SdkEventNewDeposits: // Detected deposits, as DepositInfo. Only those with IsMature set // have enough confirmations to be claimed. Show the rest as pending. newDeposits := event.NewDeposits _ = newDeposits case breez_sdk_spark.SdkEventUnclaimedDeposits: // Deposits the SDK could not claim. Each ClaimError says why, // most often the fee exceeded the configured maximum. unclaimedDeposits := event.UnclaimedDeposits _ = unclaimedDeposits case breez_sdk_spark.SdkEventClaimedDeposits: // Deposits claimed into the wallet. An instant (0-conf) claim is // reported here on submission and settles shortly after. claimedDeposits := event.ClaimedDeposits _ = claimedDeposits case breez_sdk_spark.SdkEventPaymentSucceeded: // A payment completed. The cached balance is already refreshed, // so GetInfo returns the new value. payment := event.Payment _ = payment case breez_sdk_spark.SdkEventPaymentPending: // A payment is awaiting confirmation. It arrives again as // succeeded or failed once it settles. pendingPayment := event.Payment _ = pendingPayment case breez_sdk_spark.SdkEventPaymentFailed: // A payment failed. payment.Details carries the method-specific // context to show the user. failedPayment := event.Payment _ = failedPayment case breez_sdk_spark.SdkEventAutoOptimization: // Background optimizer progress: started, round completed, or a // terminal outcome. Manual OptimizeLeaves calls do not emit these. optimizationEvent := event.OptimizationEvent _ = optimizationEvent case breez_sdk_spark.SdkEventLightningAddressChanged: // The lightning address changed on another device. Unset when the // address was deleted. lightningAddress := event.LightningAddress _ = lightningAddress case breez_sdk_spark.SdkEventUnilateralExitStateChanged: // The unilateral exit state changed, so a previously exported // one is now out of date. Export it again. default: // Handle any future event types } } func AddEventListener(sdk *breez_sdk_spark.BreezSdk, listener SdkListener) string { return sdk.AddEventListener(listener) } ``` ## Remove event listener API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.remove_event_listener When you no longer need to listen to events, you can remove the listener. ### Rust ```rust pub(crate) async fn remove_event_listener(sdk: &BreezSdk, listener_id: &str) -> Result<()> { sdk.remove_event_listener(listener_id).await; Ok(()) } ``` ### Swift ```swift func removeEventListener(sdk: BreezSdk, listenerId: String) async { await sdk.removeEventListener(id: listenerId) } ``` ### Kotlin ```kotlin suspend fun removeEventListener(sdk: BreezSdk, listenerId: String) { try { sdk.removeEventListener(listenerId) } catch (e: Exception) { // handle error } } ``` ### C# ```csharp async Task RemoveEventListener(BreezSdk sdk, string listenerId) { await sdk.RemoveEventListener(id: listenerId); } ``` ### Javascript (Wasm) ```typescript await sdk.removeEventListener(listenerId) ``` ### React Native ```typescript await sdk.removeEventListener(listenerId) ``` ### Flutter ```dart void unsubscribeFromEventStream() { _eventSubscription?.cancel(); } ``` ### Python ```python async def remove_event_listener(sdk: BreezSdk, listener_id: str): try: await sdk.remove_event_listener(id=listener_id) except Exception as error: logging.error(error) raise ``` ### Go ```go func RemoveEventListener(sdk *breez_sdk_spark.BreezSdk, listenerId string) bool { return sdk.RemoveEventListener(listenerId) } ``` --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Adding logging](/guide/logging.md)** # Adding logging API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/fn.init_logging.html The SDK implements detailed logging via a streaming interface you can manage within your application. The log entries are split into several levels that you can filter and store as desired within your application, for example, by appending them to a log file. ## Rust ```rust let data_dir_path = PathBuf::from(&data_dir); fs::create_dir_all(data_dir_path)?; init_logging(Some(data_dir), None, None)?; ``` ## Swift ```swift class SdkLogger: Logger { func log(l: LogEntry) { print("Received log [", l.level, "]: ", l.line) } } func logging() throws { try initLogging(logDir: nil, appLogger: SdkLogger(), logFilter: nil) } ``` ## Kotlin ```kotlin class SdkLogger : Logger { override fun log(l: LogEntry) { // Log.v("SDKListener", "Received log [${l.level}]: ${l.line}") } } fun setLogger(logger: SdkLogger) { try { initLogging(null, logger, null) } catch (e: Exception) { // handle error } } ``` ## C# ```csharp class SdkLogger : Logger { public void Log(LogEntry l) { Console.WriteLine($"Received log [{l.level}]: {l.line}"); } } void SetLogger(SdkLogger logger) { BreezSdkSparkMethods.InitLogging(logDir: null, appLogger: logger, logFilter: null); } ``` ## Javascript (Wasm) ```typescript class JsLogger { log = (l: LogEntry) => { console.log(`[${l.level}]: ${l.line}`) } } const logger = new JsLogger() await initLogging(logger) ``` ## React Native ```typescript class JsLogger { log = (l: LogEntry) => { console.log(`[${l.level}]: ${l.line}`) } } const logger = new JsLogger() initLogging(undefined, logger, undefined) ``` ## Flutter ```dart StreamSubscription? _logSubscription; Stream? _logStream; // Initializes SDK log stream. // // Call once on your Dart entrypoint file, e.g.; `lib/main.dart` // or singleton SDK service. It is recommended to use a single instance // of the SDK across your Flutter app. void initializeLogStream() { _logStream ??= initLogging().asBroadcastStream(); } final _logStreamController = StreamController.broadcast(); Stream get logStream => _logStreamController.stream; // Subscribe to the log stream void subscribeToLogStream() { _logSubscription = _logStream?.listen((logEntry) { _logStreamController.add(logEntry); }, onError: (e) { _logStreamController.addError(e); }); } // Unsubscribe from the log stream void unsubscribeFromLogStream() { _logSubscription?.cancel(); } ``` ## Python ```python class SdkLogger(Logger): def log(self, l: LogEntry): logging.debug(f"Received log [{l.level}]: {l.line}") def set_logger(logger: SdkLogger): try: init_logging(log_dir=None, app_logger=logger, log_filter=None) except Exception as error: logging.error(error) raise ``` ## Go ```go type SdkLogger struct{} func (SdkLogger) Log(l breez_sdk_spark.LogEntry) { log.Printf("Received log [%v]: %v", l.Level, l.Line) } func SetLogger() { var loggerImpl breez_sdk_spark.Logger = SdkLogger{} breez_sdk_spark.InitLogging(nil, &loggerImpl, nil) } ``` **→ [Spark status](/guide/spark_status.md)** # Spark status API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/fn.get_spark_status.html The SDK provides a standalone function to check the current operational status of the Spark network. This function does not require an SDK instance and can be called at any time, for example before initializing the SDK. It returns the overall status of the Spark network, along with a timestamp of when the status was last updated. The returned `ServiceStatus` has the following values: - **Operational** - All services are fully operational. - **Degraded** - Services are experiencing degraded performance. - **Partial** - Services are partially unavailable. - **Major** - Services are experiencing a major outage. - **Unknown** - Service status is unknown. ## Rust ```rust pub(crate) async fn getting_started_spark_status() -> Result<()> { let spark_status = get_spark_status(GetSparkStatusRequest::default()).await?; match spark_status.status { ServiceStatus::Operational => { info!("Spark is fully operational"); } ServiceStatus::Degraded => { info!("Spark is experiencing degraded performance"); } ServiceStatus::Partial => { info!("Spark is partially unavailable"); } ServiceStatus::Major => { info!("Spark is experiencing a major outage"); } ServiceStatus::Unknown => { info!("Spark status is unknown"); } } info!("Last updated: {}", spark_status.last_updated); Ok(()) } ``` ## Swift ```swift func gettingStartedSparkStatus() async throws { let sparkStatus = try await getSparkStatus(request: GetSparkStatusRequest()) switch sparkStatus.status { case .operational: print("Spark is fully operational") case .degraded: print("Spark is experiencing degraded performance") case .partial: print("Spark is partially unavailable") case .major: print("Spark is experiencing a major outage") case .unknown: print("Spark status is unknown") } print("Last updated: \(sparkStatus.lastUpdated)") } ``` ## Kotlin ```kotlin suspend fun gettingStartedSparkStatus() { try { val sparkStatus = getSparkStatus(GetSparkStatusRequest()) when (sparkStatus.status) { ServiceStatus.OPERATIONAL -> { // Log.v("Breez", "Spark is fully operational") } ServiceStatus.DEGRADED -> { // Log.v("Breez", "Spark is experiencing degraded performance") } ServiceStatus.PARTIAL -> { // Log.v("Breez", "Spark is partially unavailable") } ServiceStatus.MAJOR -> { // Log.v("Breez", "Spark is experiencing a major outage") } ServiceStatus.UNKNOWN -> { // Log.v("Breez", "Spark status is unknown") } } // Log.v("Breez", "Last updated: ${sparkStatus.lastUpdated}") } catch (e: Exception) { // handle error } } ``` ## C# ```csharp async Task GetSparkStatus() { var sparkStatus = await BreezSdkSparkMethods.GetSparkStatus( request: new GetSparkStatusRequest() ); switch (sparkStatus.status) { case ServiceStatus.Operational: Console.WriteLine("Spark is fully operational"); break; case ServiceStatus.Degraded: Console.WriteLine("Spark is experiencing degraded performance"); break; case ServiceStatus.Partial: Console.WriteLine("Spark is partially unavailable"); break; case ServiceStatus.Major: Console.WriteLine("Spark is experiencing a major outage"); break; case ServiceStatus.Unknown: Console.WriteLine("Spark status is unknown"); break; } Console.WriteLine($"Last updated: {sparkStatus.lastUpdated}"); } ``` ## Javascript (Wasm) ```typescript const sparkStatus = await getSparkStatus() switch (sparkStatus.status) { case 'operational': { console.log('Spark is fully operational') break } case 'degraded': { console.log('Spark is experiencing degraded performance') break } case 'partial': { console.log('Spark is partially unavailable') break } case 'major': { console.log('Spark is experiencing a major outage') break } case 'unknown': { console.log('Spark status is unknown') break } } console.log(`Last updated: ${sparkStatus.lastUpdated}`) ``` ## React Native ```typescript const sparkStatus = await getSparkStatus({ proxy: undefined }) switch (sparkStatus.status) { case ServiceStatus.Operational: console.log('Spark is fully operational') break case ServiceStatus.Degraded: console.log('Spark is experiencing degraded performance') break case ServiceStatus.Partial: console.log('Spark is partially unavailable') break case ServiceStatus.Major: console.log('Spark is experiencing a major outage') break case ServiceStatus.Unknown: console.log('Spark status is unknown') break } console.log(`Last updated: ${sparkStatus.lastUpdated}`) ``` ## Flutter ```dart Future gettingStartedSparkStatus() async { final sparkStatus = await getSparkStatus(request: GetSparkStatusRequest()); switch (sparkStatus.status) { case ServiceStatus.operational: print("Spark is fully operational"); break; case ServiceStatus.degraded: print("Spark is experiencing degraded performance"); break; case ServiceStatus.partial: print("Spark is partially unavailable"); break; case ServiceStatus.major: print("Spark is experiencing a major outage"); break; case ServiceStatus.unknown: print("Spark status is unknown"); break; } print("Last updated: ${sparkStatus.lastUpdated}"); } ``` ## Python ```python async def getting_started_spark_status(): try: spark_status = await get_spark_status(request=GetSparkStatusRequest()) if spark_status.status == ServiceStatus.OPERATIONAL: logging.debug("Spark is fully operational") elif spark_status.status == ServiceStatus.DEGRADED: logging.debug("Spark is experiencing degraded performance") elif spark_status.status == ServiceStatus.PARTIAL: logging.debug("Spark is partially unavailable") elif spark_status.status == ServiceStatus.MAJOR: logging.debug("Spark is experiencing a major outage") elif spark_status.status == ServiceStatus.UNKNOWN: logging.debug("Spark status is unknown") logging.debug(f"Last updated: {spark_status.last_updated}") except Exception as error: logging.error(error) raise ``` ## Go ```go func GetSparkStatus() error { request := breez_sdk_spark.GetSparkStatusRequest{} sparkStatus, err := breez_sdk_spark.GetSparkStatus(request) if err != nil { return err } switch sparkStatus.Status { case breez_sdk_spark.ServiceStatusOperational: log.Printf("Spark is fully operational") case breez_sdk_spark.ServiceStatusDegraded: log.Printf("Spark is experiencing degraded performance") case breez_sdk_spark.ServiceStatusPartial: log.Printf("Spark is partially unavailable") case breez_sdk_spark.ServiceStatusMajor: log.Printf("Spark is experiencing a major outage") case breez_sdk_spark.ServiceStatusUnknown: log.Printf("Spark status is unknown") } log.Printf("Last updated: %v", sparkStatus.LastUpdated) return nil } ``` **→ [Payment fundamentals](/guide/payments.md)** # Payment fundamentals How to send, receive and manage payments using the Breez SDK - Spark: - **[Parsing inputs]** to determine the payment type - **[Receiving payments]** via a Lightning invoice, Bitcoin address or Spark address - **[Sending payments]** via a Lightning invoice, Bitcoin address or Spark address - **[Listing payments]** that have been sent or received in the past - **[Claiming on-chain deposits]** to manually claim or refund Bitcoin payments [Parsing inputs]: parse.md [Receiving payments]: receive_payment.md [Sending payments]: send_payment.md [Listing payments]: list_payments.md [Claiming on-chain deposits]: onchain_claims.md ## Further reading - **[Using LNURL and Lightning addresses](/guide/lnurl.md)** - **[Handling tokens](/guide/tokens.md)** **→ [Parsing inputs](/guide/parse.md)** # Parsing inputs The SDK provides a versatile and extensible parsing module designed to process a wide range of input strings and return parsed data in various standardized formats. Natively supported formats include: BOLT11 invoices, LNURLs of different types, Bitcoin addresses, Spark addresses, and others. For the complete list, consult the [API documentation](https://breez.github.io/spark-sdk/breez_sdk_spark/enum.InputType.html). Cross-chain destinations on EVM, Solana, and Tron — bare addresses or chain-prefixed URIs — parse to `InputType::CrossChainAddress`, carrying the parsed address family along with any token contract address and amount embedded in the URI. Use the resulting `CrossChainAddressDetails` to discover available routes; see [Send USDC/USDT](/guide/send_payment.md#usdc-usdt) for the send flow. **Developer note** The amounts returned from calling parse on Lightning based inputs (BOLT11, LNURL) are denominated in millisatoshi. ## Rust ```rust let input = "an input to be parsed..."; match sdk.parse(input).await? { InputType::BitcoinAddress(details) => { println!("Input is Bitcoin address {}", details.address); } InputType::Bolt11Invoice(details) => { println!( "Input is BOLT11 invoice for {} msats", details .amount_msat .map_or("unknown".to_string(), |a| a.to_string()) ); } InputType::LnurlPay(details) => { println!( "Input is LNURL-Pay/Lightning address accepting min/max {}/{} msats", details.min_sendable, details.max_sendable ); } InputType::LnurlWithdraw(details) => { println!( "Input is LNURL-Withdraw for min/max {}/{} msats", details.min_withdrawable, details.max_withdrawable ); } InputType::SparkAddress(details) => { println!("Input is Spark address {}", details.address); } InputType::SparkInvoice(invoice) => { println!("Input is Spark invoice:"); if let Some(token_identifier) = &invoice.token_identifier { println!( " Amount: {:?} base units of token with id {}", invoice.amount, token_identifier ); } else { println!(" Amount: {:?} sats", invoice.amount); } if let Some(description) = &invoice.description { println!(" Description: {}", description); } if let Some(expiry_time) = invoice.expiry_time { println!(" Expiry time: {}", expiry_time); } if let Some(sender_public_key) = &invoice.sender_public_key { println!(" Sender public key: {}", sender_public_key); } } InputType::CrossChainAddress(details) => { println!( "Input is cross-chain address {} ({:?})", details.address, details.address_family ); } // Other input types are available _ => {} } ``` ## Swift ```swift let input = "an input to be parsed..." do { let inputType = try await sdk.parse(input: input) switch inputType { case .bitcoinAddress(v1: let details): print("Input is Bitcoin address \(details.address)") case .bolt11Invoice(v1: let details): let amount = details.amountMsat.map { String($0) } ?? "unknown" print("Input is BOLT11 invoice for \(amount) msats") case .lnurlPay(v1: let details): print( "Input is LNURL-Pay/Lightning address accepting min/max " + "\(details.minSendable)/\(details.maxSendable) msats)" ) case .lnurlWithdraw(v1: let details): print( "Input is LNURL-Withdraw for min/max " + "\(details.minWithdrawable)/\(details.maxWithdrawable) msats" ) case .sparkAddress(v1: let details): print("Input is Spark address \(details.address)") case .sparkInvoice(v1: let invoice): print("Input is Spark invoice:") if let tokenIdentifier = invoice.tokenIdentifier { print(" Amount: \(invoice.amount) base units of token with id \(tokenIdentifier)") } else { print(" Amount: \(invoice.amount) sats") } if let description = invoice.description { print(" Description: \(description)") } if let expiryTime = invoice.expiryTime { print(" Expiry time: \(Date(timeIntervalSince1970: TimeInterval(expiryTime)))") } if let senderPublicKey = invoice.senderPublicKey { print(" Sender public key: \(senderPublicKey)") } case .crossChainAddress(v1: let details): print("Input is cross-chain address \(details.address) (\(details.addressFamily))") default: break // Other input types are available } } catch { print("Failed to parse input: \(error)") } ``` ## Kotlin ```kotlin val input = "an input to be parsed..." try { val inputType = sdk.parse(input) when (inputType) { is InputType.BitcoinAddress -> { println("Input is Bitcoin address ${inputType.v1.address}") } is InputType.Bolt11Invoice -> { val amountStr = inputType.v1.amountMsat?.toString() ?: "unknown" println("Input is BOLT11 invoice for $amountStr msats") } is InputType.LnurlPay -> { println( "Input is LNURL-Pay/Lightning address accepting min/max " + "${inputType.v1.minSendable}/${inputType.v1.maxSendable} msats}" ) } is InputType.LnurlWithdraw -> { println( "Input is LNURL-Withdraw for min/max " + "${inputType.v1.minWithdrawable}/${inputType.v1.maxWithdrawable} msats" ) } is InputType.SparkAddress -> { println("Input is Spark address ${inputType.v1.address}") } is InputType.SparkInvoice -> { val invoice = inputType.v1 println("Input is Spark invoice:") if (invoice.tokenIdentifier != null) { println( " Amount: ${invoice.amount} base units of token " + "with id ${invoice.tokenIdentifier}" ) } else { println(" Amount: ${invoice.amount} sats") } if (invoice.description != null) { println(" Description: ${invoice.description}") } if (invoice.expiryTime != null) { println(" Expiry time: ${invoice.expiryTime}") } if (invoice.senderPublicKey != null) { println(" Sender public key: ${invoice.senderPublicKey}") } } is InputType.CrossChainAddress -> { val details = inputType.v1 println( "Input is cross-chain address ${details.address} " + "(${details.addressFamily})" ) } else -> { // Handle other input types } } } catch (e: Exception) { // handle error } ``` ## C# ```csharp var inputStr = "an input to be parsed..."; var parsedInput = await sdk.Parse(input: inputStr); switch (parsedInput) { case InputType.BitcoinAddress bitcoinAddress: var details = bitcoinAddress.v1; Console.WriteLine($"Input is Bitcoin address {details.address}"); break; case InputType.Bolt11Invoice bolt11: var bolt11Details = bolt11.v1; var amount = bolt11Details.amountMsat.HasValue ? bolt11Details.amountMsat.Value.ToString() : "unknown"; Console.WriteLine($"Input is BOLT11 invoice for {amount} msats"); break; case InputType.LnurlPay lnurlPay: var lnurlPayDetails = lnurlPay.v1; Console.WriteLine($"Input is LNURL-Pay/Lightning address accepting " + $"min/max {lnurlPayDetails.minSendable}/" + $"{lnurlPayDetails.maxSendable} msats"); break; case InputType.LnurlWithdraw lnurlWithdraw: var lnurlWithdrawDetails = lnurlWithdraw.v1; Console.WriteLine($"Input is LNURL-Withdraw for min/max " + $"{lnurlWithdrawDetails.minWithdrawable}/" + $"{lnurlWithdrawDetails.maxWithdrawable} msats"); break; case InputType.SparkAddress sparkAddress: var sparkAddressDetails = sparkAddress.v1; Console.WriteLine($"Input is Spark address {sparkAddressDetails.address}"); break; case InputType.SparkInvoice sparkInvoice: var invoice = sparkInvoice.v1; Console.WriteLine("Input is Spark invoice:"); if (invoice.tokenIdentifier != null) { Console.WriteLine($" Amount: {invoice.amount} base units of " + $"token with id {invoice.tokenIdentifier}"); } else { Console.WriteLine($" Amount: {invoice.amount} sats"); } if (invoice.description != null) { Console.WriteLine($" Description: {invoice.description}"); } if (invoice.expiryTime.HasValue) { Console.WriteLine($" Expiry time: {invoice.expiryTime}"); } if (invoice.senderPublicKey != null) { Console.WriteLine($" Sender public key: {invoice.senderPublicKey}"); } break; case InputType.CrossChainAddress crossChainAddress: var crossChainDetails = crossChainAddress.v1; Console.WriteLine($"Input is cross-chain address {crossChainDetails.address} " + $"({crossChainDetails.addressFamily})"); break; // Other input types are available } ``` ## Javascript (Wasm) ```typescript const input = 'an input to be parsed...' const parsed = await sdk.parse(input) switch (parsed.type) { case 'bitcoinAddress': console.log(`Input is Bitcoin address ${parsed.address}`) break case 'bolt11Invoice': console.log( `Input is BOLT11 invoice for ${ parsed.amountMsat != null ? parsed.amountMsat.toString() : 'unknown' } msats` ) break case 'lnurlPay': console.log( 'Input is LNURL-Pay/Lightning address accepting min/max ' + `${parsed.minSendable}/${parsed.maxSendable} msats` ) break case 'lnurlWithdraw': console.log( 'Input is LNURL-Withdraw for min/max ' + `${parsed.minWithdrawable}/${parsed.maxWithdrawable} msats` ) break case 'sparkAddress': console.log(`Input is Spark address ${parsed.address}`) break case 'sparkInvoice': console.log('Input is Spark invoice:') if (parsed.tokenIdentifier != null) { console.log( ` Amount: ${parsed.amount} base units of token with id ${parsed.tokenIdentifier}` ) } else { console.log(` Amount: ${parsed.amount} sats`) } if (parsed.description != null) { console.log(` Description: ${parsed.description}`) } if (parsed.expiryTime != null) { console.log(` Expiry time: ${new Date(Number(parsed.expiryTime) * 1000).toISOString()}`) } if (parsed.senderPublicKey != null) { console.log(` Sender public key: ${parsed.senderPublicKey}`) } break case 'crossChainAddress': console.log(`Input is cross-chain address ${parsed.address} (${parsed.addressFamily})`) break default: // Other input types are available break } ``` ## React Native ```typescript const inputStr = 'an input to be parsed...' const input = await sdk.parse(inputStr) if (input.tag === InputType_Tags.BitcoinAddress) { console.log(`Input is Bitcoin address ${input.inner[0].address}`) } else if (input.tag === InputType_Tags.Bolt11Invoice) { console.log( `Input is BOLT11 invoice for ${ input.inner[0].amountMsat != null ? input.inner[0].amountMsat.toString() : 'unknown' } msats` ) } else if (input.tag === InputType_Tags.LnurlPay) { console.log( 'Input is LNURL-Pay/Lightning address accepting min/max ' + `${input.inner[0].minSendable}/${input.inner[0].maxSendable} msats` ) } else if (input.tag === InputType_Tags.LnurlWithdraw) { console.log( 'Input is LNURL-Withdraw for min/max ' + `${input.inner[0].minWithdrawable}/${input.inner[0].maxWithdrawable} msats` ) } else if (input.tag === InputType_Tags.SparkAddress) { console.log(`Input is Spark address ${input.inner[0].address}`) } else if (input.tag === InputType_Tags.SparkInvoice) { const invoice = input.inner[0] console.log('Input is Spark invoice:') if (invoice.tokenIdentifier != null) { console.log( ` Amount: ${invoice.amount} base units of token with id ${invoice.tokenIdentifier}` ) } else { console.log(` Amount: ${invoice.amount} sats`) } if (invoice.description != null) { console.log(` Description: ${invoice.description}`) } if (invoice.expiryTime != null) { console.log(` Expiry time: ${new Date(Number(invoice.expiryTime) * 1000).toISOString()}`) } if (invoice.senderPublicKey != null) { console.log(` Sender public key: ${invoice.senderPublicKey}`) } } else if (input.tag === InputType_Tags.CrossChainAddress) { const details = input.inner[0] console.log(`Input is cross-chain address ${details.address} (${details.addressFamily})`) } else { // Other input types are available } ``` ## Flutter ```dart String input = "an input to be parsed..."; InputType inputType = await sdk.parse(input: input); if (inputType is InputType_BitcoinAddress) { print("Input is Bitcoin address ${inputType.field0.address}"); } else if (inputType is InputType_Bolt11Invoice) { String amountStr = inputType.field0.amountMsat != null ? inputType.field0.amountMsat.toString() : "unknown"; print("Input is BOLT11 invoice for $amountStr msats"); } else if (inputType is InputType_LnurlPay) { print("Input is LNURL-Pay/Lightning address accepting min/max " "${inputType.field0.minSendable}/${inputType.field0.maxSendable} msats"); } else if (inputType is InputType_LnurlWithdraw) { print("Input is LNURL-Withdraw for min/max " "${inputType.field0.minWithdrawable}/${inputType.field0.maxWithdrawable} msats"); } else if (inputType is InputType_SparkAddress) { print("Input is Spark address ${inputType.field0.address}"); } else if (inputType is InputType_SparkInvoice) { var invoice = inputType.field0; print("Input is Spark invoice:"); if (invoice.tokenIdentifier != null) { print(" Amount: ${invoice.amount} base units of token with id ${invoice.tokenIdentifier}"); } else { print(" Amount: ${invoice.amount} sats"); } if (invoice.description != null) { print(" Description: ${invoice.description}"); } if (invoice.expiryTime != null) { print(" Expiry time: " "${DateTime.fromMillisecondsSinceEpoch(invoice.expiryTime!.toInt() * 1000)}"); } if (invoice.senderPublicKey != null) { print(" Sender public key: ${invoice.senderPublicKey}"); } } else if (inputType is InputType_CrossChainAddress) { var details = inputType.field0; print("Input is cross-chain address ${details.address} (${details.addressFamily})"); } else { // Other input types are available } ``` ## Python ```python input_str = "an input to be parsed..." try: parsed_input = await sdk.parse(input=input_str) if isinstance(parsed_input, InputType.BITCOIN_ADDRESS): details = parsed_input[0] logging.debug(f"Input is Bitcoin address {details.address}") elif isinstance(parsed_input, InputType.BOLT11_INVOICE): details = parsed_input[0] amount = "unknown" if details.amount_msat: amount = str(details.amount_msat) logging.debug(f"Input is BOLT11 invoice for {amount} msats") elif isinstance(parsed_input, InputType.LNURL_PAY): details = parsed_input[0] logging.debug( f"Input is LNURL-Pay/Lightning address accepting " f"min/max {details.min_sendable}/{details.max_sendable} msats" ) elif isinstance(parsed_input, InputType.LNURL_WITHDRAW): details = parsed_input[0] logging.debug( f"Input is LNURL-Withdraw for min/max " f"{details.min_withdrawable}/{details.max_withdrawable} msats" ) elif isinstance(parsed_input, InputType.SPARK_ADDRESS): details = parsed_input[0] logging.debug(f"Input is Spark address {details.address}") elif isinstance(parsed_input, InputType.SPARK_INVOICE): invoice = parsed_input[0] logging.debug("Input is Spark invoice:") if invoice.token_identifier: logging.debug(f" Amount: {invoice.amount} base units of " f"token with id {invoice.token_identifier}") else: logging.debug(f" Amount: {invoice.amount} sats") if invoice.description: logging.debug(f" Description: {invoice.description}") if invoice.expiry_time: logging.debug(f" Expiry time: {invoice.expiry_time}") if invoice.sender_public_key: logging.debug(f" Sender public key: {invoice.sender_public_key}") elif isinstance(parsed_input, InputType.CROSS_CHAIN_ADDRESS): details = parsed_input[0] logging.debug( f"Input is cross-chain address {details.address} ({details.address_family})" ) # Other input types are available except Exception as error: logging.error(error) raise ``` ## Go ```go inputStr := "an input to be parsed..." input, err := sdk.Parse(inputStr) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } switch inputType := input.(type) { case breez_sdk_spark.InputTypeBitcoinAddress: log.Printf("Input is Bitcoin address %s", inputType.Field0.Address) case breez_sdk_spark.InputTypeBolt11Invoice: amount := "unknown" if inputType.Field0.AmountMsat != nil { amount = strconv.FormatUint(*inputType.Field0.AmountMsat, 10) } log.Printf("Input is BOLT11 invoice for %s msats", amount) case breez_sdk_spark.InputTypeLnurlPay: log.Printf("Input is LNURL-Pay/Lightning address accepting min/max %d/%d msats", inputType.Field0.MinSendable, inputType.Field0.MaxSendable) case breez_sdk_spark.InputTypeLnurlWithdraw: log.Printf("Input is LNURL-Withdraw for min/max %d/%d msats", inputType.Field0.MinWithdrawable, inputType.Field0.MaxWithdrawable) case breez_sdk_spark.InputTypeSparkAddress: log.Printf("Input is Spark address %s", inputType.Field0.Address) case breez_sdk_spark.InputTypeSparkInvoice: invoice := inputType.Field0 log.Println("Input is Spark invoice:") if invoice.TokenIdentifier != nil { log.Printf( " Amount: %d base units of token with id %s", invoice.Amount, *invoice.TokenIdentifier, ) } else { log.Printf(" Amount: %d sats", invoice.Amount) } if invoice.Description != nil { log.Printf(" Description: %s", *invoice.Description) } if invoice.ExpiryTime != nil { log.Printf(" Expiry time: %d", *invoice.ExpiryTime) } if invoice.SenderPublicKey != nil { log.Printf(" Sender public key: %s", *invoice.SenderPublicKey) } case breez_sdk_spark.InputTypeCrossChainAddress: details := inputType.Field0 log.Printf( "Input is cross-chain address %s (%v)", details.Address, details.AddressFamily, ) default: // Other input types are available } ``` ## Supporting other input formats The parsing module can be extended using external input parsers provided in the SDK configuration. These will be used when the input is not recognized. You can implement and provide your own parsers, or use existing public ones. ### Configuring external parsers Configuring external parsers can only be done before [initializing](/guide/initializing.md#basic-initialization) and the config cannot be changed through the lifetime of the connection. Multiple parsers can be configured, and each one is defined by: - **Provider ID**: an arbitrary id to identify the provider input type - **Input regex**: a regex pattern that should reliably match all inputs that this parser can process, even if it may also match some invalid inputs - **Parser URL**: an URL containing the placeholder `` When parsing an input that isn't recognized as one of the native input types, the SDK will check if the input conforms to any of the external parsers regex expressions. If so, it will make an HTTP `GET` request to the provided URL, replacing the placeholder with the input. If the input is recognized, the response should include in its body a string that can be parsed into one of the natively supported types. #### Rust ```rust // Create the default config let mut config = default_config(Network::Mainnet); config.api_key = Some("".to_string()); // Configure external parsers config.external_input_parsers = Some(vec![ ExternalInputParser { provider_id: "provider_a".to_string(), input_regex: "^provider_a".to_string(), parser_url: "https://parser-domain.com/parser?input=".to_string(), }, ExternalInputParser { provider_id: "provider_b".to_string(), input_regex: "^provider_b".to_string(), parser_url: "https://parser-domain.com/parser?input=".to_string(), }, ]); ``` #### Swift ```swift // Create the default config var config = defaultConfig(network: Network.mainnet) config.apiKey = "" // Configure external parsers config.externalInputParsers = [ ExternalInputParser( providerId: "provider_a", inputRegex: "^provider_a", parserUrl: "https://parser-domain.com/parser?input=" ), ExternalInputParser( providerId: "provider_b", inputRegex: "^provider_b", parserUrl: "https://parser-domain.com/parser?input=" ), ] ``` #### Kotlin ```kotlin // Create the default config val config = defaultConfig(Network.MAINNET) config.apiKey = "" // Configure external parsers config.externalInputParsers = listOf( ExternalInputParser( providerId = "provider_a", inputRegex = "^provider_a", parserUrl = "https://parser-domain.com/parser?input=" ), ExternalInputParser( providerId = "provider_b", inputRegex = "^provider_b", parserUrl = "https://parser-domain.com/parser?input=" ) ) ``` #### C# ```csharp // Create the default config var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { apiKey = "", externalInputParsers = new ExternalInputParser[] { new ExternalInputParser( providerId: "provider_a", inputRegex: "^provider_a", parserUrl: "https://parser-domain.com/parser?input=" ), new ExternalInputParser( providerId: "provider_b", inputRegex: "^provider_b", parserUrl: "https://parser-domain.com/parser?input=" ) } }; ``` #### Javascript (Wasm) ```typescript // Create the default config const config = defaultConfig('mainnet') config.apiKey = '' // Configure external parsers config.externalInputParsers = [ { providerId: 'provider_a', inputRegex: '^provider_a', parserUrl: 'https://parser-domain.com/parser?input=' }, { providerId: 'provider_b', inputRegex: '^provider_b', parserUrl: 'https://parser-domain.com/parser?input=' } ] ``` #### React Native ```typescript // Create the default config const config = defaultConfig(Network.Mainnet) config.apiKey = '' // Configure external parsers config.externalInputParsers = [ { providerId: 'provider_a', inputRegex: '^provider_a', parserUrl: 'https://parser-domain.com/parser?input=' }, { providerId: 'provider_b', inputRegex: '^provider_b', parserUrl: 'https://parser-domain.com/parser?input=' } ] ``` #### Flutter ```dart // Create the default config Config config = defaultConfig(network: Network.mainnet) .copyWith(apiKey: ""); config = config.copyWith( externalInputParsers: [ ExternalInputParser( providerId: "provider_a", inputRegex: "^provider_a", parserUrl: "https://parser-domain.com/parser?input=", ), ExternalInputParser( providerId: "provider_b", inputRegex: "^provider_b", parserUrl: "https://parser-domain.com/parser?input=", ), ], ); ``` #### Python ```python # Create the default config config = default_config(network=Network.MAINNET) config.api_key = "" # Configure external parsers config.external_input_parsers = [ ExternalInputParser( provider_id="provider_a", input_regex="^provider_a", parser_url="https://parser-domain.com/parser?input=" ), ExternalInputParser( provider_id="provider_b", input_regex="^provider_b", parser_url="https://parser-domain.com/parser?input=" ) ] ``` #### Go ```go // Create the default config apiKey := "" config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) config.ApiKey = &apiKey // Configure external parsers parsers := []breez_sdk_spark.ExternalInputParser{ { ProviderId: "provider_a", InputRegex: "^provider_a", ParserUrl: "https://parser-domain.com/parser?input=", }, { ProviderId: "provider_b", InputRegex: "^provider_b", ParserUrl: "https://parser-domain.com/parser?input=", }, } config.ExternalInputParsers = &parsers ``` ### Public external parsers - [**PicknPay QRs**](https://www.pnp.co.za/) - Maintainer: [MoneyBadger](https://www.moneybadger.co.za/) - Regex: `(.*)(za.co.electrum.picknpay)(.*)` - URL: `https://cryptoqr.net/.well-known/lnurlp/` - More info: [support+breezsdk@moneybadger.co.za](mailto:support+breezsdk@moneybadger.co.za) - [**Bootlegger QRs**](https://www.bootlegger.coffee/) - Maintainer: [MoneyBadger](https://www.moneybadger.co.za/) - Regex: `(.*)(wigroup\.co|yoyogroup\.co)(.*)` - URL: `https://cryptoqr.net/.well-known/lnurlw/` - More info: [support+breezsdk@moneybadger.co.za](mailto:support+breezsdk@moneybadger.co.za) ### Default external parsers The SDK ships with some embedded default external parsers. If you prefer not to use them, you can disable them in the SDK's configuration. See the available default parsers in the [API Documentation](https://breez.github.io/spark-sdk/breez_sdk_spark/constant.DEFAULT_EXTERNAL_INPUT_PARSERS.html) by checking the source of the constant. --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Receiving payments](/guide/receive_payment.md)** # Receiving payments API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.receive_payment Once the SDK is initialized, you can directly begin receiving payments. The SDK supports receiving via Lightning, Bitcoin, Spark, and USDC/USDT into Spark from a supported external chain. ## Lightning #### BOLT11 invoice When receiving via Lightning, we can generate a BOLT11 invoice to be paid. Setting the invoice amount fixes the amount the sender should pay. To create an invoice for another Spark wallet, set `receiver_identity_public_key` to that wallet's identity public key. Creating the invoice requires only the receiver's public key, not their private keys. **Note:** the payment may fallback to a direct Spark payment (if the payer's client supports this). ##### Rust ```rust let description = "".to_string(); // Optionally set the invoice amount you wish the payer to send let optional_amount_sats = Some(5_000); // Optionally set the expiry duration in seconds let optional_expiry_secs = Some(3600_u32); // Set this to create an invoice for another Spark identity let optional_receiver_identity_public_key = None; let response = sdk .receive_payment(ReceivePaymentRequest { payment_method: ReceivePaymentMethod::Bolt11Invoice { description, amount_sats: optional_amount_sats, expiry_secs: optional_expiry_secs, payment_hash: None, receiver_identity_public_key: optional_receiver_identity_public_key, }, }) .await?; let payment_request = response.payment_request; info!("Payment request: {payment_request}"); let receive_fee_sats = response.fee; info!("Fees: {receive_fee_sats} sats"); ``` ##### Swift ```swift let description = "" // Optionally set the invoice amount you wish the payer to send let optionalAmountSats: UInt64 = 5_000 // Optionally set the expiry duration in seconds let optionalExpirySecs: UInt32 = 3600 let optionalReceiverIdentityPublicKey: String? = nil let response = try await sdk .receivePayment( request: ReceivePaymentRequest( paymentMethod: ReceivePaymentMethod.bolt11Invoice( description: description, amountSats: optionalAmountSats, expirySecs: optionalExpirySecs, paymentHash: nil, receiverIdentityPublicKey: optionalReceiverIdentityPublicKey ) )) let paymentRequest = response.paymentRequest print("Payment Request: {}", paymentRequest) let receiveFeeSats = response.fee print("Fees: {} sats", receiveFeeSats) ``` ##### Kotlin ```kotlin try { val description = "" // Optionally set the invoice amount you wish the payer to send val optionalAmountSats = 5_000.toULong() // Optionally set the expiry duration in seconds val optionalExpirySecs = 3600.toUInt() val optionalReceiverIdentityPublicKey: String? = null val request = ReceivePaymentRequest( ReceivePaymentMethod.Bolt11Invoice( description, optionalAmountSats, optionalExpirySecs, null, optionalReceiverIdentityPublicKey ) ) val response = sdk.receivePayment(request) val paymentRequest = response.paymentRequest // Log.v("Breez", "Payment Request: ${paymentRequest}") val receiveFeeSats = response.fee // Log.v("Breez", "Fees: ${receiveFeeSats} sats") } catch (e: Exception) { // handle error } ``` ##### C# ```csharp var description = ""; // Optionally set the invoice amount you wish the payer to send var optionalAmountSats = 5_000UL; // Optionally set the expiry duration in seconds var optionalExpirySecs = 3600U; string? optionalReceiverIdentityPublicKey = null; var paymentMethod = new ReceivePaymentMethod.Bolt11Invoice( description: description, amountSats: optionalAmountSats, expirySecs: optionalExpirySecs, paymentHash: null, receiverIdentityPublicKey: optionalReceiverIdentityPublicKey ); var request = new ReceivePaymentRequest(paymentMethod: paymentMethod); var response = await sdk.ReceivePayment(request: request); var paymentRequest = response.paymentRequest; Console.WriteLine($"Payment Request: {paymentRequest}"); var receiveFeeSats = response.fee; Console.WriteLine($"Fees: {receiveFeeSats} sats"); ``` ##### Javascript (Wasm) ```typescript const description = '' // Optionally set the invoice amount you wish the payer to send const optionalAmountSats = 5_000 // Optionally set the expiry duration in seconds const optionalExpirySecs = 3600 // Set this to create an invoice for another Spark identity const optionalReceiverIdentityPublicKey = undefined const response = await sdk.receivePayment({ paymentMethod: { type: 'bolt11Invoice', description, amountSats: optionalAmountSats, expirySecs: optionalExpirySecs, paymentHash: undefined, receiverIdentityPublicKey: optionalReceiverIdentityPublicKey } }) const paymentRequest = response.paymentRequest console.log(`Payment Request: ${paymentRequest}`) const receiveFeeSats = response.fee console.log(`Fees: ${receiveFeeSats} sats`) ``` ##### React Native ```typescript const description = '' // Optionally set the invoice amount you wish the payer to send const optionalAmountSats = BigInt(5_000) // Optionally set the expiry duration in seconds const optionalExpirySecs = 3600 // Set this to create an invoice for another Spark identity const optionalReceiverIdentityPublicKey = undefined const response = await sdk.receivePayment({ paymentMethod: new ReceivePaymentMethod.Bolt11Invoice({ description, amountSats: optionalAmountSats, expirySecs: optionalExpirySecs, paymentHash: undefined, receiverIdentityPublicKey: optionalReceiverIdentityPublicKey }) }) const paymentRequest = response.paymentRequest console.log(`Payment Request: ${paymentRequest}`) const receiveFeeSats = response.fee console.log(`Fees: ${receiveFeeSats} sats`) ``` ##### Flutter ```dart String description = ""; // Optionally set the invoice amount you wish the payer to send BigInt optionalAmountSats = BigInt.from(5000); // Optionally set the expiry duration in seconds int optionalExpirySecs = 3600; // Set this to create an invoice for another Spark identity String? optionalReceiverIdentityPublicKey; // Create an invoice and set the amount you wish the payer to send ReceivePaymentRequest request = ReceivePaymentRequest( paymentMethod: ReceivePaymentMethod.bolt11Invoice( description: description, amountSats: optionalAmountSats, expirySecs: optionalExpirySecs, paymentHash: null, receiverIdentityPublicKey: optionalReceiverIdentityPublicKey)); ReceivePaymentResponse response = await sdk.receivePayment( request: request, ); String paymentRequest = response.paymentRequest; print("Payment request: $paymentRequest"); BigInt receiveFeeSats = response.fee; print("Fees: $receiveFeeSats sats"); ``` ##### Python ```python try: description = "" # Optionally set the invoice amount you wish the payer to send optional_amount_sats = 5_000 # Optionally set the expiry duration in seconds optional_expiry_secs = 3600 # Set this to create an invoice for another Spark identity optional_receiver_identity_public_key = None payment_method = ReceivePaymentMethod.BOLT11_INVOICE( description=description, amount_sats=optional_amount_sats, expiry_secs=optional_expiry_secs, payment_hash=None, receiver_identity_public_key=optional_receiver_identity_public_key, ) request = ReceivePaymentRequest(payment_method=payment_method) response = await sdk.receive_payment(request=request) payment_request = response.payment_request logging.debug(f"Payment Request: {payment_request}") receive_fee_sats = response.fee logging.debug(f"Fees: {receive_fee_sats} sats") return response except Exception as error: logging.error(error) raise ``` ##### Go ```go description := "" // Optionally set the invoice amount you wish the payer to send optionalAmountSats := uint64(5_000) // Optionally set the expiry duration in seconds optionalExpirySecs := uint32(3600) // Set this to create an invoice for another Spark identity var optionalReceiverIdentityPublicKey *string request := breez_sdk_spark.ReceivePaymentRequest{ PaymentMethod: breez_sdk_spark.ReceivePaymentMethodBolt11Invoice{ Description: description, AmountSats: &optionalAmountSats, ExpirySecs: &optionalExpirySecs, PaymentHash: nil, ReceiverIdentityPublicKey: optionalReceiverIdentityPublicKey, }, } response, err := sdk.ReceivePayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } paymentRequest := response.PaymentRequest log.Printf("Payment Request: %v", paymentRequest) receiveFeesSat := response.Fee log.Printf("Fees: %v sats", receiveFeesSat) ``` #### LNURL-Pay & Lightning address To receive via LNURL-Pay and/or a Lightning address, follow [these instructions](/guide/receive_lnurl_pay.md). > Note: Lightning payments work in Spark even if the receiver is offline. To understand how it works under the hood, read [this](https://docs.spark.money/learn/lightning). ## Bitcoin For on-chain payments you can generate a Bitcoin deposit address to receive payments. By default the existing address is returned; you can optionally request a new address to rotate to a fresh one for improved privacy. All previously generated addresses remain monitored. On-chain deposits go through the following lifecycle: 1. **Detected** — The SDK detects the deposit and emits a `SdkEvent::NewDeposits` event. The deposit may or may not have sufficient confirmations to be claimed yet. 2. **Sufficient confirmations** — After **3 on-chain confirmations**, the deposit has sufficient confirmations and the SDK automatically attempts to claim it. 3. **Claimed or unclaimed** — If claiming succeeds, the funds are added to your balance. If it fails (e.g. fees too high), the deposit remains unclaimed and can be [manually claimed or refunded](/guide/onchain_claims.md). ### Rust ```rust let new_address = None; // Set to Some(true) to get a new address let response = sdk .receive_payment(ReceivePaymentRequest { payment_method: ReceivePaymentMethod::BitcoinAddress { new_address }, }) .await?; let payment_request = response.payment_request; info!("Payment request: {payment_request}"); let receive_fee_sats = response.fee; info!("Fees: {receive_fee_sats} sats"); ``` ### Swift ```swift let newAddress: Bool? = nil // Set to true to get a new address let response = try await sdk .receivePayment( request: ReceivePaymentRequest( paymentMethod: ReceivePaymentMethod.bitcoinAddress( newAddress: newAddress) )) let paymentRequest = response.paymentRequest print("Payment Request: {}", paymentRequest) let receiveFeeSats = response.fee print("Fees: {} sats", receiveFeeSats) ``` ### Kotlin ```kotlin try { val newAddress: Boolean? = null // Set to true to get a new address val request = ReceivePaymentRequest( ReceivePaymentMethod.BitcoinAddress(newAddress = newAddress) ) val response = sdk.receivePayment(request) val paymentRequest = response.paymentRequest // Log.v("Breez", "Payment Request: ${paymentRequest}") val receiveFeeSats = response.fee // Log.v("Breez", "Fees: ${receiveFeeSats} sats") } catch (e: Exception) { // handle error } ``` ### C# ```csharp bool? newAddress = null; // Set to true to get a new address var request = new ReceivePaymentRequest( paymentMethod: new ReceivePaymentMethod.BitcoinAddress( newAddress: newAddress) ); var response = await sdk.ReceivePayment(request: request); var paymentRequest = response.paymentRequest; Console.WriteLine($"Payment Request: {paymentRequest}"); var receiveFeeSats = response.fee; Console.WriteLine($"Fees: {receiveFeeSats} sats"); ``` ### Javascript (Wasm) ```typescript const newAddress = undefined // Set to true to get a new address const response = await sdk.receivePayment({ paymentMethod: { type: 'bitcoinAddress', newAddress } }) const paymentRequest = response.paymentRequest console.log(`Payment Request: ${paymentRequest}`) const receiveFeeSats = response.fee console.log(`Fees: ${receiveFeeSats} sats`) ``` ### React Native ```typescript const newAddress = undefined // Set to true to get a new address const response = await sdk.receivePayment({ paymentMethod: new ReceivePaymentMethod.BitcoinAddress({ newAddress }) }) const paymentRequest = response.paymentRequest console.log(`Payment Request: ${paymentRequest}`) const receiveFeeSats = response.fee console.log(`Fees: ${receiveFeeSats} sats`) ``` ### Flutter ```dart bool? newAddress; // Set to true to get a new address ReceivePaymentRequest request = ReceivePaymentRequest( paymentMethod: ReceivePaymentMethod.bitcoinAddress( newAddress: newAddress)); ReceivePaymentResponse response = await sdk.receivePayment( request: request, ); String paymentRequest = response.paymentRequest; print("Payment request: $paymentRequest"); BigInt receiveFeeSats = response.fee; print("Fees: $receiveFeeSats sats"); ``` ### Python ```python try: new_address = None # Set to True to get a new address request = ReceivePaymentRequest( payment_method=ReceivePaymentMethod.BITCOIN_ADDRESS( new_address=new_address) ) response = await sdk.receive_payment(request=request) payment_request = response.payment_request logging.debug(f"Payment Request: {payment_request}") receive_fee_sats = response.fee logging.debug(f"Fees: {receive_fee_sats} sats") return response except Exception as error: logging.error(error) raise ``` ### Go ```go var newAddress *bool // To get a new address: t := true; newAddress = &t request := breez_sdk_spark.ReceivePaymentRequest{ PaymentMethod: breez_sdk_spark.ReceivePaymentMethodBitcoinAddress{ NewAddress: newAddress, }, } response, err := sdk.ReceivePayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } paymentRequest := response.PaymentRequest log.Printf("Payment Request: %v", paymentRequest) receiveFeesSat := response.Fee log.Printf("Fees: %v sats", receiveFeesSat) ``` To track pending deposits, use `list_unclaimed_deposits` and filter by the `is_mature` field: ### Rust ```rust let request = ListUnclaimedDepositsRequest {}; let response = sdk.list_unclaimed_deposits(request).await?; let pending_deposits: Vec<&DepositInfo> = response.deposits.iter().filter(|d| !d.is_mature).collect(); for deposit in pending_deposits { info!("Pending deposit: {}:{}", deposit.txid, deposit.vout); info!("Amount: {} sats", deposit.amount_sats); } ``` ### Swift ```swift let request = ListUnclaimedDepositsRequest() let response = try await sdk.listUnclaimedDeposits(request: request) let pendingDeposits = response.deposits.filter { !$0.isMature } for deposit in pendingDeposits { print("Pending deposit: \(deposit.txid):\(deposit.vout)") print("Amount: \(deposit.amountSats) sats") } ``` ### Kotlin ```kotlin try { val request = ListUnclaimedDepositsRequest val response = sdk.listUnclaimedDeposits(request) val pendingDeposits = response.deposits.filter { !it.isMature } for (deposit in pendingDeposits) { // Log.v("Breez", "Pending deposit: ${deposit.txid}:${deposit.vout}") // Log.v("Breez", "Amount: ${deposit.amountSats} sats") } } catch (e: Exception) { // handle error } ``` ### C# ```csharp var request = new ListUnclaimedDepositsRequest(); var response = await sdk.ListUnclaimedDeposits(request: request); var pendingDeposits = response.deposits.Where(d => !d.isMature).ToList(); foreach (var deposit in pendingDeposits) { Console.WriteLine($"Pending deposit: {deposit.txid}:{deposit.vout}"); Console.WriteLine($"Amount: {deposit.amountSats} sats"); } ``` ### Javascript (Wasm) ```typescript const request: ListUnclaimedDepositsRequest = {} const response = await sdk.listUnclaimedDeposits(request) const pendingDeposits = response.deposits.filter((d) => !d.isMature) for (const deposit of pendingDeposits) { console.log(`Pending deposit: ${deposit.txid}:${deposit.vout}`) console.log(`Amount: ${deposit.amountSats} sats`) } ``` ### React Native ```typescript const request: ListUnclaimedDepositsRequest = {} const response = await sdk.listUnclaimedDeposits(request) const pendingDeposits = response.deposits.filter((d) => !d.isMature) for (const deposit of pendingDeposits) { console.log(`Pending deposit: ${deposit.txid}:${deposit.vout}`) console.log(`Amount: ${deposit.amountSats} sats`) } ``` ### Flutter ```dart final request = ListUnclaimedDepositsRequest(); final response = await sdk.listUnclaimedDeposits(request: request); final pendingDeposits = response.deposits.where((d) => !d.isMature).toList(); for (DepositInfo deposit in pendingDeposits) { print("Pending deposit: ${deposit.txid}:${deposit.vout}"); print("Amount: ${deposit.amountSats} sats"); } ``` ### Python ```python try: request = ListUnclaimedDepositsRequest() response = await sdk.list_unclaimed_deposits(request=request) pending_deposits = [d for d in response.deposits if not d.is_mature] for deposit in pending_deposits: logging.info(f"Pending deposit: {deposit.txid}:{deposit.vout}") logging.info(f"Amount: {deposit.amount_sats} sats") except Exception as error: logging.error(error) raise ``` ### Go ```go request := breez_sdk_spark.ListUnclaimedDepositsRequest{} response, err := sdk.ListUnclaimedDeposits(request) if err != nil { return err } var pendingDeposits []breez_sdk_spark.DepositInfo for _, deposit := range response.Deposits { if !deposit.IsMature { pendingDeposits = append(pendingDeposits, deposit) } } for _, deposit := range pendingDeposits { log.Printf("Pending deposit: %v:%v", deposit.Txid, deposit.Vout) log.Printf("Amount: %v sats", deposit.AmountSats) } ``` ## Spark For payments between Spark users, you can use a Spark address or generate a Spark invoice to receive payments. #### Spark address Spark addresses are static. ##### Rust ```rust let response = sdk .receive_payment(ReceivePaymentRequest { payment_method: ReceivePaymentMethod::SparkAddress, }) .await?; let payment_request = response.payment_request; info!("Payment request: {payment_request}"); let receive_fee_sats = response.fee; info!("Fees: {receive_fee_sats} sats"); ``` ##### Swift ```swift let response = try await sdk .receivePayment( request: ReceivePaymentRequest( paymentMethod: ReceivePaymentMethod.sparkAddress )) let paymentRequest = response.paymentRequest print("Payment Request: {}", paymentRequest) let receiveFeeSats = response.fee print("Fees: {} sats", receiveFeeSats) ``` ##### Kotlin ```kotlin try { val request = ReceivePaymentRequest(ReceivePaymentMethod.SparkAddress) val response = sdk.receivePayment(request) val paymentRequest = response.paymentRequest // Log.v("Breez", "Payment Request: ${paymentRequest}") val receiveFeeSats = response.fee // Log.v("Breez", "Fees: ${receiveFeeSats} sats") } catch (e: Exception) { // handle error } ``` ##### C# ```csharp var request = new ReceivePaymentRequest( paymentMethod: new ReceivePaymentMethod.SparkAddress() ); var response = await sdk.ReceivePayment(request: request); var paymentRequest = response.paymentRequest; Console.WriteLine($"Payment Request: {paymentRequest}"); var receiveFeeSats = response.fee; Console.WriteLine($"Fees: {receiveFeeSats} sats"); ``` ##### Javascript (Wasm) ```typescript const response = await sdk.receivePayment({ paymentMethod: { type: 'sparkAddress' } }) const paymentRequest = response.paymentRequest console.log(`Payment Request: ${paymentRequest}`) const receiveFeeSats = response.fee console.log(`Fees: ${receiveFeeSats} sats`) ``` ##### React Native ```typescript const response = await sdk.receivePayment({ paymentMethod: new ReceivePaymentMethod.SparkAddress() }) const paymentRequest = response.paymentRequest console.log(`Payment Request: ${paymentRequest}`) const receiveFeeSats = response.fee console.log(`Fees: ${receiveFeeSats} sats`) ``` ##### Flutter ```dart ReceivePaymentRequest request = ReceivePaymentRequest(paymentMethod: ReceivePaymentMethod.sparkAddress()); ReceivePaymentResponse response = await sdk.receivePayment( request: request, ); String paymentRequest = response.paymentRequest; print("Payment request: $paymentRequest"); BigInt receiveFeeSats = response.fee; print("Fees: $receiveFeeSats sats"); ``` ##### Python ```python try: request = ReceivePaymentRequest( payment_method=ReceivePaymentMethod.SPARK_ADDRESS() ) response = await sdk.receive_payment(request=request) payment_request = response.payment_request logging.debug(f"Payment Request: {payment_request}") receive_fee_sats = response.fee logging.debug(f"Fees: {receive_fee_sats} sats") return response except Exception as error: logging.error(error) raise ``` ##### Go ```go request := breez_sdk_spark.ReceivePaymentRequest{ PaymentMethod: breez_sdk_spark.ReceivePaymentMethodSparkAddress{}, } response, err := sdk.ReceivePayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } paymentRequest := response.PaymentRequest log.Printf("Payment Request: %v", paymentRequest) receiveFeesSat := response.Fee log.Printf("Fees: %v sats", receiveFeesSat) ``` #### Spark invoice Spark invoices are single-use and may impose restrictions on the payment, such as amount, expiry, and who is able to pay it. ##### Rust ```rust let optional_description = "".to_string(); let optional_amount_sats = Some(5_000); // Optionally set the expiry UNIX timestamp in seconds let optional_expiry_time_seconds = Some(1716691200); let optional_sender_public_key = Some("".to_string()); let response = sdk .receive_payment(ReceivePaymentRequest { payment_method: ReceivePaymentMethod::SparkInvoice { token_identifier: None, description: Some(optional_description), amount: optional_amount_sats, expiry_time: optional_expiry_time_seconds, sender_public_key: optional_sender_public_key, }, }) .await?; let payment_request = response.payment_request; info!("Payment request: {payment_request}"); let receive_fee_sats = response.fee; info!("Fees: {receive_fee_sats} sats"); ``` ##### Swift ```swift let optionalDescription = "" let optionalAmountSats = BInt(5_000) // Optionally set the expiry UNIX timestamp in seconds let optionalExpiryTimeSeconds: UInt64 = 1_716_691_200 let optionalSenderPublicKey = "" let response = try await sdk .receivePayment( request: ReceivePaymentRequest( paymentMethod: ReceivePaymentMethod.sparkInvoice( amount: optionalAmountSats, tokenIdentifier: nil, expiryTime: optionalExpiryTimeSeconds, description: optionalDescription, senderPublicKey: optionalSenderPublicKey ) )) let paymentRequest = response.paymentRequest print("Payment Request: {}", paymentRequest) let receiveFeeSats = response.fee print("Fees: {} sats", receiveFeeSats) ``` ##### Kotlin ```kotlin try { val optionalDescription = "" // Kotlin MPP (BigInteger from com.ionspin.kotlin.bignum.integer, which is included in // package) val optionalAmountSats = BigInteger.fromLong(5_000L) // Android (BigInteger from java.math) // val optionalAmountSats = BigInteger.valueOf(5_000L) // Optionally set the expiry UNIX timestamp in seconds val optionalExpiryTimeSeconds = 1716691200.toULong() val optionalSenderPublicKey = "" val request = ReceivePaymentRequest( ReceivePaymentMethod.SparkInvoice( tokenIdentifier = null, description = optionalDescription, amount = optionalAmountSats, expiryTime = optionalExpiryTimeSeconds, senderPublicKey = optionalSenderPublicKey ) ) val response = sdk.receivePayment(request) val paymentRequest = response.paymentRequest // Log.v("Breez", "Payment Request: ${paymentRequest}") val receiveFeeSats = response.fee // Log.v("Breez", "Fees: ${receiveFeeSats} sats") } catch (e: Exception) { // handle error } ``` ##### C# ```csharp var optionalDescription = ""; var optionalAmountSats = new BigInteger(5000); // Optionally set the expiry UNIX timestamp in seconds var optionalExpiryTimeSeconds = 1716691200UL; var optionalSenderPublicKey = ""; var request = new ReceivePaymentRequest( paymentMethod: new ReceivePaymentMethod.SparkInvoice( description: optionalDescription, amount: optionalAmountSats, expiryTime: optionalExpiryTimeSeconds, senderPublicKey: optionalSenderPublicKey, tokenIdentifier: null ) ); var response = await sdk.ReceivePayment(request: request); var paymentRequest = response.paymentRequest; Console.WriteLine($"Payment Request: {paymentRequest}"); var receiveFeeSats = response.fee; Console.WriteLine($"Fees: {receiveFeeSats} sats"); ``` ##### Javascript (Wasm) ```typescript const optionalDescription = '' const optionalAmountSats = '5000' // Optionally set the expiry UNIX timestamp in seconds const optionalExpiryTimeSeconds = 1716691200 const optionalSenderPublicKey = '' const response = await sdk.receivePayment({ paymentMethod: { type: 'sparkInvoice', description: optionalDescription, amount: optionalAmountSats, expiryTime: optionalExpiryTimeSeconds, senderPublicKey: optionalSenderPublicKey } }) const paymentRequest = response.paymentRequest console.log(`Payment Request: ${paymentRequest}`) const receiveFeeSats = response.fee console.log(`Fees: ${receiveFeeSats} sats`) ``` ##### React Native ```typescript const optionalDescription = '' const optionalAmountSats = BigInt(5_000) // Optionally set the expiry UNIX timestamp in seconds const optionalExpiryTimeSeconds = BigInt(1716691200) const optionalSenderPublicKey = '' const response = await sdk.receivePayment({ paymentMethod: new ReceivePaymentMethod.SparkInvoice({ description: optionalDescription, amount: optionalAmountSats, expiryTime: optionalExpiryTimeSeconds, senderPublicKey: optionalSenderPublicKey, tokenIdentifier: undefined }) }) const paymentRequest = response.paymentRequest console.log(`Payment Request: ${paymentRequest}`) const receiveFeeSats = response.fee console.log(`Fees: ${receiveFeeSats} sats`) ``` ##### Flutter ```dart String optionalDescription = ""; BigInt optionalAmountSats = BigInt.from(5000); // Optionally set the expiry UNIX timestamp in seconds BigInt optionalExpiryTimeSeconds = BigInt.from(1716691200); String optionalSenderPublicKey = ""; ReceivePaymentRequest request = ReceivePaymentRequest(paymentMethod: ReceivePaymentMethod.sparkInvoice( description: optionalDescription, amount: optionalAmountSats, expiryTime: optionalExpiryTimeSeconds, senderPublicKey: optionalSenderPublicKey, )); ReceivePaymentResponse response = await sdk.receivePayment( request: request, ); String paymentRequest = response.paymentRequest; print("Payment request: $paymentRequest"); BigInt receiveFeeSats = response.fee; print("Fees: $receiveFeeSats sats"); ``` ##### Python ```python try: optional_description = "" optional_amount_sats = 5_000 # Optionally set the expiry UNIX timestamp in seconds optional_expiry_time_seconds = 1716691200 optional_sender_public_key = "" request = ReceivePaymentRequest( payment_method=ReceivePaymentMethod.SPARK_INVOICE( description=optional_description, amount=optional_amount_sats, expiry_time=optional_expiry_time_seconds, sender_public_key=optional_sender_public_key, token_identifier=None, ) ) response = await sdk.receive_payment(request=request) payment_request = response.payment_request logging.debug(f"Payment Request: {payment_request}") receive_fee_sats = response.fee logging.debug(f"Fees: {receive_fee_sats} sats") return response except Exception as error: logging.error(error) raise ``` ##### Go ```go optionalDescription := "" optionalAmountSats := new(big.Int).SetInt64(5_000) // Optionally set the expiry UNIX timestamp in seconds optionalExpiryTimeSeconds := uint64(1716691200) optionalSenderPublicKey := "" request := breez_sdk_spark.ReceivePaymentRequest{ PaymentMethod: breez_sdk_spark.ReceivePaymentMethodSparkInvoice{ Description: &optionalDescription, Amount: &optionalAmountSats, ExpiryTime: &optionalExpiryTimeSeconds, SenderPublicKey: &optionalSenderPublicKey, }, } response, err := sdk.ReceivePayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } paymentRequest := response.PaymentRequest log.Printf("Payment Request: %v", paymentRequest) receiveFeesSat := response.Fee log.Printf("Fees: %v sats", receiveFeesSat) ``` ## USDC/USDT Cross-chain receive is supported only via the Orchestra provider. Receive USDC or USDT from a sender on one of several supported chains: Ethereum-family chains (Arbitrum, Base, and similar EVM networks), Solana, and Tron. The receiver lands either BTC sats or [USDB](/guide/stable_balance.md) (a 6-decimal USD-pegged token on Spark) on the Spark side. This feature must be enabled in [the SDK configuration](/guide/config.md#usdc-usdt) before using. See [USDC/USDT](/guide/cross_chain.md) for provider details and the status lifecycle. Call `get_cross_chain_routes` with `CrossChainRouteFilter::Receive` to discover supported source assets. Each `CrossChainRoutePair` names the provider, source chain and asset, decimals, optional token contract address, and the Spark-side destinations the route lands (`CrossChainRoutePair.accepted_assets`). ### Rust ```rust let routes = sdk .get_cross_chain_routes(&CrossChainRouteFilter::Receive { contract_address: None, }) .await?; for route in &routes { info!( "Route via {:?}: {}/{} -> Spark", route.provider, route.chain, route.asset ); } ``` ### Swift ```swift let routes = try await sdk.getCrossChainRoutes( filter: .receive(contractAddress: nil)) for route in routes { print("Route via \(route.provider): \(route.chain)/\(route.asset) -> Spark") } ``` ### Kotlin ```kotlin try { val routes = sdk.getCrossChainRoutes( CrossChainRouteFilter.Receive(contractAddress = null) ) for (route in routes) { println("Route via ${route.provider}: ${route.chain}/${route.asset} -> Spark") } } catch (e: Exception) { // handle error } ``` ### C# ```csharp var filter = new CrossChainRouteFilter.Receive(contractAddress: null); var routes = await sdk.GetCrossChainRoutes(filter: filter); foreach (var route in routes) { Console.WriteLine( $"Route via {route.provider}: {route.chain}/{route.asset} -> Spark" ); } ``` ### Javascript (Wasm) ```typescript const routes = await sdk.getCrossChainRoutes({ type: 'receive', contractAddress: undefined }) for (const route of routes) { console.debug( `Route via ${route.provider}: ${route.chain}/${route.asset} -> Spark` ) } ``` ### React Native ```typescript const routes = await sdk.getCrossChainRoutes( new CrossChainRouteFilter.Receive({ contractAddress: undefined }) ) for (const route of routes) { console.debug( `Route via ${route.provider}: ${route.chain}/${route.asset} -> Spark` ) } ``` ### Flutter ```dart List routes = await sdk.getCrossChainRoutes( filter: CrossChainRouteFilter.receive(contractAddress: null), ); for (var route in routes) { print( "Route via ${route.provider}: ${route.chain}/${route.asset} -> Spark", ); } ``` ### Python ```python try: routes = await sdk.get_cross_chain_routes( filter=CrossChainRouteFilter.RECEIVE(contract_address=None) ) for route in routes: logging.debug( f"Route via {route.provider}: {route.chain}/{route.asset} -> Spark" ) except Exception as error: logging.error(error) raise ``` ### Go ```go filter := breez_sdk_spark.CrossChainRouteFilterReceive{ContractAddress: nil} routes, err := sdk.GetCrossChainRoutes(filter) if err != nil { return nil, err } for _, route := range routes { log.Printf( "Route via %v: %s/%s -> Spark", route.Provider, route.Chain, route.Asset, ) } ``` Build `ReceivePaymentMethod::CrossChain` with the chosen route and an `amount`. The `amount` on {{#name ReceivePaymentMethod::CrossChain}} is in the source asset's base units, per the route's `CrossChainRoutePair.decimals`. USD-stable sources sit at USD parity, so `1_000_000` is 1 USDC (6 decimals), about $1. `fee_mode` controls what the amount means: - `CrossChainFeeMode::FeesExcluded` (default): `amount` is the receiver's target on Spark. The SDK pads the sender's deposit to cover provider fees plus an overpay buffer. - `CrossChainFeeMode::FeesIncluded`: `amount` is the deposit the sender pays. The receiver lands `amount - fees`. `destination` picks which Spark-side asset the receiver wants delivered. Left unset, the SDK auto-picks the wallet's active stable-balance token if the route supports it, otherwise BTC (converted from the USD amount via the live BTC/USD rate). `max_slippage_bps` (10 to 500) bounds the price movement tolerated between quote and delivery. `target_overpay_bps` (0 to 500) sets the FeesExcluded overpay buffer. Left unset, the SDK defaults apply. The `payment_request` field carries an EIP-681 URI for EVM routes and the bare deposit address for Solana and Tron. The `cross_chain_info` block surfaces the bare deposit address, deposit amount, expected receive amount, destination denomination, and quote `expires_at`. The receiver pays no fee; the sender's deposit covers it. ### Rust ```rust // amount is in the route's source-asset base units (USD-stable parity: // 1_000_000 = $1 on 6-decimal routes). See the guide for fee_mode, // destination, and the slippage/overpay overrides. let amount = 1_000_000u128; let optional_destination: Option = None; let optional_max_slippage_bps = Some(100); let optional_target_overpay_bps: Option = None; let optional_fee_mode: Option = None; let response = sdk .receive_payment(ReceivePaymentRequest { payment_method: ReceivePaymentMethod::CrossChain { route, amount, destination: optional_destination, fee_mode: optional_fee_mode, max_slippage_bps: optional_max_slippage_bps, target_overpay_bps: optional_target_overpay_bps, }, }) .await?; info!("Payment request: {}", response.payment_request); if let Some(info) = response.cross_chain_info { info!("Deposit address: {}", info.deposit_address); info!("Deposit amount: {}", info.deposit_amount); info!( "Expected received: {} {}", info.expected_received_amount, info.destination_asset ); info!("Expires at: {}", info.expires_at); } ``` ### Swift ```swift // amount is in the route's source-asset base units (USD-stable parity: // 1_000_000 = $1 on 6-decimal routes). See the guide for feeMode, // destination, and the slippage/overpay overrides. let amount = BInt(1_000_000) let optionalDestination: SparkAsset? = nil let optionalMaxSlippageBps: UInt32? = 100 let optionalTargetOverpayBps: UInt32? = nil let optionalFeeMode: CrossChainFeeMode? = nil let response = try await sdk.receivePayment( request: ReceivePaymentRequest( paymentMethod: .crossChain( route: route, amount: amount, destination: optionalDestination, feeMode: optionalFeeMode, maxSlippageBps: optionalMaxSlippageBps, targetOverpayBps: optionalTargetOverpayBps ) )) print("Payment request: \(response.paymentRequest)") if let info = response.crossChainInfo { print("Deposit address: \(info.depositAddress)") print("Deposit amount: \(info.depositAmount)") print( "Expected received: \(info.expectedReceivedAmount) " + "\(info.destinationAsset)" ) print("Expires at: \(info.expiresAt)") } ``` ### Kotlin ```kotlin // amount is in the route's source-asset base units (USD-stable parity: // 1_000_000 = $1 on 6-decimal routes). See the guide for feeMode, // destination, and the slippage/overpay overrides. val amount = BigInteger.fromLong(1_000_000L) val optionalDestination: SparkAsset? = null val optionalMaxSlippageBps: UInt? = 100u val optionalTargetOverpayBps: UInt? = null val optionalFeeMode: CrossChainFeeMode? = null try { val req = ReceivePaymentRequest( paymentMethod = ReceivePaymentMethod.CrossChain( route = route, amount = amount, destination = optionalDestination, feeMode = optionalFeeMode, maxSlippageBps = optionalMaxSlippageBps, targetOverpayBps = optionalTargetOverpayBps, ), ) val response = sdk.receivePayment(req) println("Payment request: ${response.paymentRequest}") val info = response.crossChainInfo if (info != null) { println("Deposit address: ${info.depositAddress}") println("Deposit amount: ${info.depositAmount}") println( "Expected received: ${info.expectedReceivedAmount} " + "${info.destinationAsset}", ) println("Expires at: ${info.expiresAt}") } } catch (e: Exception) { // handle error } ``` ### C# ```csharp // amount is in the route's source-asset base units (USD-stable // parity: 1_000_000 = $1 on 6-decimal routes). See the guide for // feeMode, destination, and the slippage/overpay overrides. var amount = new BigInteger(1_000_000); SparkAsset? optionalDestination = null; uint? optionalMaxSlippageBps = 100; uint? optionalTargetOverpayBps = null; CrossChainFeeMode? optionalFeeMode = null; var request = new ReceivePaymentRequest( paymentMethod: new ReceivePaymentMethod.CrossChain( route: route, amount: amount, destination: optionalDestination, feeMode: optionalFeeMode, maxSlippageBps: optionalMaxSlippageBps, targetOverpayBps: optionalTargetOverpayBps ) ); var response = await sdk.ReceivePayment(request: request); Console.WriteLine($"Payment request: {response.paymentRequest}"); if (response.crossChainInfo is { } info) { Console.WriteLine($"Deposit address: {info.depositAddress}"); Console.WriteLine($"Deposit amount: {info.depositAmount}"); Console.WriteLine( "Expected received: " + $"{info.expectedReceivedAmount} {info.destinationAsset}" ); Console.WriteLine($"Expires at: {info.expiresAt}"); } ``` ### Javascript (Wasm) ```typescript // amount is in the route's source-asset base units (USD-stable parity: // 1_000_000 = $1 on 6-decimal routes). See the guide for feeMode, // destination, and the slippage/overpay overrides. const amount = '1000000' const optionalDestination = undefined const optionalMaxSlippageBps = 100 const optionalTargetOverpayBps = undefined const optionalFeeMode = undefined const response = await sdk.receivePayment({ paymentMethod: { type: 'crossChain', route, amount, destination: optionalDestination, feeMode: optionalFeeMode, maxSlippageBps: optionalMaxSlippageBps, targetOverpayBps: optionalTargetOverpayBps } }) console.debug(`Payment request: ${response.paymentRequest}`) if (response.crossChainInfo !== undefined) { const { depositAddress, depositAmount, expectedReceivedAmount, destinationAsset, expiresAt } = response.crossChainInfo console.debug(`Deposit address: ${depositAddress}`) console.debug(`Deposit amount: ${depositAmount}`) console.debug( `Expected received: ${expectedReceivedAmount} ${destinationAsset}` ) console.debug(`Expires at: ${expiresAt}`) } ``` ### React Native ```typescript // amount is in the route's source-asset base units (USD-stable parity: // 1_000_000 = $1 on 6-decimal routes). See the guide for feeMode, // destination, and the slippage/overpay overrides. const amount = BigInt(1_000_000) const optionalDestination: SparkAsset | undefined = undefined const optionalMaxSlippageBps = 100 const optionalTargetOverpayBps = undefined const optionalFeeMode = undefined const response = await sdk.receivePayment({ paymentMethod: new ReceivePaymentMethod.CrossChain({ route, amount, destination: optionalDestination, feeMode: optionalFeeMode, maxSlippageBps: optionalMaxSlippageBps, targetOverpayBps: optionalTargetOverpayBps }) }) console.debug(`Payment request: ${response.paymentRequest}`) if (response.crossChainInfo !== undefined) { const { depositAddress, depositAmount, expectedReceivedAmount, destinationAsset, expiresAt } = response.crossChainInfo console.debug(`Deposit address: ${depositAddress}`) console.debug(`Deposit amount: ${depositAmount}`) console.debug( `Expected received: ${expectedReceivedAmount} ${destinationAsset}` ) console.debug(`Expires at: ${expiresAt}`) } ``` ### Flutter ```dart // amount is in the route's source-asset base units (USD-stable parity: // 1_000_000 = $1 on 6-decimal routes). See the guide for feeMode, // destination, and the slippage/overpay overrides. final amount = BigInt.from(1000000); SparkAsset? optionalDestination; int? optionalMaxSlippageBps = 100; int? optionalTargetOverpayBps; CrossChainFeeMode? optionalFeeMode; final request = ReceivePaymentRequest( paymentMethod: ReceivePaymentMethod.crossChain( route: route, amount: amount, destination: optionalDestination, feeMode: optionalFeeMode, maxSlippageBps: optionalMaxSlippageBps, targetOverpayBps: optionalTargetOverpayBps, ), ); final response = await sdk.receivePayment(request: request); print("Payment request: ${response.paymentRequest}"); final info = response.crossChainInfo; if (info != null) { print("Deposit address: ${info.depositAddress}"); print("Deposit amount: ${info.depositAmount}"); print( "Expected received: ${info.expectedReceivedAmount} ${info.destinationAsset}", ); print("Expires at: ${info.expiresAt}"); } ``` ### Python ```python # amount is in the route's source-asset base units (USD-stable parity: # 1_000_000 = $1 on 6-decimal routes). See the guide for fee_mode, # destination, and the slippage/overpay overrides. amount = 1_000_000 optional_destination = None optional_max_slippage_bps = 100 optional_target_overpay_bps = None optional_fee_mode = None try: request = ReceivePaymentRequest( payment_method=ReceivePaymentMethod.CROSS_CHAIN( route=route, amount=amount, destination=optional_destination, fee_mode=optional_fee_mode, max_slippage_bps=optional_max_slippage_bps, target_overpay_bps=optional_target_overpay_bps, ) ) response = await sdk.receive_payment(request=request) logging.debug(f"Payment request: {response.payment_request}") info = response.cross_chain_info if info is not None: logging.debug(f"Deposit address: {info.deposit_address}") logging.debug(f"Deposit amount: {info.deposit_amount}") logging.debug( f"Expected received: {info.expected_received_amount} " f"{info.destination_asset}" ) logging.debug(f"Expires at: {info.expires_at}") except Exception as error: logging.error(error) raise ``` ### Go ```go // amount is in the route's source-asset base units (USD-stable parity: // 1_000_000 = $1 on 6-decimal routes). See the guide for FeeMode, // destination, and the slippage/overpay overrides. amount := new(big.Int).SetInt64(1_000_000) var optionalDestination *breez_sdk_spark.SparkAsset = nil optionalMaxSlippageBps := uint32(100) var optionalTargetOverpayBps *uint32 = nil var optionalFeeMode *breez_sdk_spark.CrossChainFeeMode = nil request := breez_sdk_spark.ReceivePaymentRequest{ PaymentMethod: breez_sdk_spark.ReceivePaymentMethodCrossChain{ Route: route, Amount: amount, Destination: optionalDestination, FeeMode: optionalFeeMode, MaxSlippageBps: &optionalMaxSlippageBps, TargetOverpayBps: optionalTargetOverpayBps, }, } response, err := sdk.ReceivePayment(request) if err != nil { return nil, err } log.Printf("Payment request: %s", response.PaymentRequest) if info := response.CrossChainInfo; info != nil { log.Printf("Deposit address: %s", info.DepositAddress) log.Printf("Deposit amount: %v", info.DepositAmount) log.Printf( "Expected received: %v %s", info.ExpectedReceivedAmount, info.DestinationAsset, ) log.Printf("Expires at: %d", info.ExpiresAt) } ``` ## Event Flows Once a receive payment is initiated, you can follow and react to the different payment events using the guide below for each payment method. See [listening to events](/guide/events.md) for how to subscribe to events. The `SdkEvent::Synced` event is also emitted as the SDK syncs in the background. See [fetching the balance](/guide/get_info.md) for the recommended pattern for refreshing the balance and payments list. #### Lightning | Event | Description | UX Suggestion | | -------------------- | ----------------------------------------------------------------- | ------------------------------------------------ | | **PaymentPending** | The Spark transfer was detected and the claim process will start. | Show payment as pending. | | **PaymentSucceeded** | The Spark transfer is claimed and the payment is complete. | Show the payment as complete and call `get_info` to read the updated balance. The SDK refreshes the cached balance before emitting this event. See [fetching the balance](/guide/get_info.md). | #### Bitcoin The following events are emitted in order during the deposit lifecycle. See [Listening to events](/guide/events.md) for how to subscribe. | Event | Description | UX Suggestion | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | **NewDeposits** | New deposits were detected. Each deposit includes a `is_mature` field indicating whether it has enough confirmations to be claimed. | Show the deposit to the user. If it does not yet have sufficient confirmations, show it as pending. | | **ClaimedDeposits** | The SDK successfully claimed confirmed deposits. | | | **UnclaimedDeposits** | Claiming failed (e.g. fee exceeded the configured maximum or the UTXO could not be found). | Allow the user to manually claim or refund. See [Claiming on-chain deposits](/guide/onchain_claims.md). | | **PaymentPending** | The Spark transfer was detected and the claim process will start. | Show payment as pending. | | **PaymentSucceeded** | The Spark transfer is claimed and the payment is complete. | Show the payment as complete and call `get_info` to read the updated balance. The SDK refreshes the cached balance before emitting this event. See [fetching the balance](/guide/get_info.md). | #### Spark | Event | Description | UX Suggestion | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | | **PaymentPending** | The Spark transfer was detected and the claim process will start. For Spark HTLC payments, the claim will only start once the HTLC is claimed. For more details see [Spark HTLC payments](/guide/htlcs.md). | Show payment as pending. | | **PaymentSucceeded** | The Spark transfer is claimed and the payment is complete. | Show the payment as complete and call `get_info` to read the updated balance. The SDK refreshes the cached balance before emitting this event. See [fetching the balance](/guide/get_info.md). | #### USDC/USDT | Event | Description | UX Suggestion | | -------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------ | | **PaymentPending** | The sender's deposit was detected and the inbound Spark transfer claim is in progress. | Show payment as pending. | | **PaymentSucceeded** | The inbound Spark transfer is claimed and the payment is complete. | Show the payment as complete and call `get_info` to read the updated balance. The SDK refreshes the cached balance before emitting this event. See [fetching the balance](/guide/get_info.md). | --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Sending payments](/guide/send_payment.md)** # Sending payments Once the SDK is initialized, you can directly begin sending payments. The send process takes two steps: 1. [Preparing the Payment](/guide/send_payment.md#preparing-payments) 2. [Sending the Payment](/guide/send_payment.md#sending-payments) For sending payments via LNURL, see [LNURL-Pay](/guide/lnurl_pay.md). ## Preparing Payments API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.prepare_send_payment During the prepare step, the SDK ensures that the inputs are valid with respect to the payment request type, and also returns the fees related to the payment so they can be confirmed. The payment request field supports Lightning invoices, Bitcoin addresses, Spark addresses and Spark invoices. **Developer note** Payments can be sent without holding Bitcoin by converting on-the-fly as a step before sending a payment. See Converting tokens for more information. ### Lightning #### BOLT11 invoice For BOLT11 invoices the amount can be optionally set. It is only required if the invoice doesn't specify an amount. If the invoice specifies an amount, providing a different amount is not supported. If the invoice also contains a Spark address, the payment can be sent directly via a Spark transfer instead. When this is the case, the prepare response includes the Spark transfer fee. Note that only one fee is paid: either the Lightning fee or the Spark transfer fee, depending on which payment method is ultimately used. See [Lightning](/guide/send_payment.md#lightning-1) for how to select the payment method. ##### Rust ```rust let payment_request = "".to_string(); // Optionally set the amount you wish to pay the receiver let optional_amount_sats = Some(5_000); let prepare_response = sdk .prepare_send_payment(PrepareSendPaymentRequest { payment_request: PaymentRequest::Input { input: payment_request, }, amount: optional_amount_sats, token_identifier: None, conversion_options: None, fee_policy: None, }) .await?; // If the fees are acceptable, continue to create the Send Payment if let SendPaymentMethod::Bolt11Invoice { spark_transfer_fee_sats, lightning_fee_sats, .. } = prepare_response.payment_method { // Fees to pay via Lightning info!("Lightning Fees: {lightning_fee_sats} sats"); // Or fees to pay (if available) via a Spark transfer info!("Spark Transfer Fees: {spark_transfer_fee_sats:?} sats"); } ``` ##### Swift ```swift let paymentRequest = "" // Optionally set the amount you wish to pay the receiver let optionalAmountSats: BInt? = BInt(5_000) let prepareResponse = try await sdk.prepareSendPayment( request: PrepareSendPaymentRequest( paymentRequest: .input(input: paymentRequest), amount: optionalAmountSats, tokenIdentifier: nil, conversionOptions: nil, feePolicy: nil )) if case let .bolt11Invoice(_, sparkTransferFeeSats, lightningFeeSats) = prepareResponse .paymentMethod { // Fees to pay via Lightning print("Lightning Fees: \(lightningFeeSats) sats") // Or fees to pay (if available) via a Spark transfer if let sparkTransferFeeSats = sparkTransferFeeSats { print("Spark Transfer Fees: \(sparkTransferFeeSats) sats") } } ``` ##### Kotlin ```kotlin val paymentRequest = "" // Optionally set the amount you wish to pay the receiver // Kotlin MPP (BigInteger from com.ionspin.kotlin.bignum.integer) val optionalAmountSats = BigInteger.fromLong(5_000L) // Android (BigInteger from java.math) // val optionalAmountSats = BigInteger.valueOf(5_000L) try { val req = PrepareSendPaymentRequest( paymentRequest = PaymentRequest.Input(input = paymentRequest), amount = optionalAmountSats, tokenIdentifier = null, conversionOptions = null, feePolicy = null, ) val prepareResponse = sdk.prepareSendPayment(req) // If the fees are acceptable, continue to create the Send Payment val paymentMethod = prepareResponse.paymentMethod if (paymentMethod is SendPaymentMethod.Bolt11Invoice) { // Fees to pay via Lightning val lightningFeeSats = paymentMethod.lightningFeeSats // Or fees to pay (if available) via a Spark transfer val sparkTransferFeeSats = paymentMethod.sparkTransferFeeSats // Log.v("Breez", "Lightning Fees: ${lightningFeeSats} sats") // Log.v("Breez", "Spark Transfer Fees: ${sparkTransferFeeSats} sats") } } catch (e: Exception) { // handle error } ``` ##### C# ```csharp var paymentRequest = ""; // Optionally set the amount you wish to pay the receiver ulong? optionalAmountSats = 5_000UL; var request = new PrepareSendPaymentRequest( paymentRequest: new PaymentRequest.Input(input: paymentRequest), amount: optionalAmountSats, tokenIdentifier: null, conversionOptions: null, feePolicy: null ); var prepareResponse = await sdk.PrepareSendPayment(request: request); // If the fees are acceptable, continue to create the Send Payment if (prepareResponse.paymentMethod is SendPaymentMethod.Bolt11Invoice bolt11Method) { // Fees to pay via Lightning var lightningFeeSats = bolt11Method.lightningFeeSats; // Or fees to pay (if available) via a Spark transfer var sparkTransferFeeSats = bolt11Method.sparkTransferFeeSats; Console.WriteLine($"Lightning Fees: {lightningFeeSats} sats"); Console.WriteLine($"Spark Transfer Fees: {sparkTransferFeeSats} sats"); } ``` ##### Javascript (Wasm) ```typescript const paymentRequest = '' // Optionally set the amount you wish to pay the receiver const optionalAmountSats = BigInt(5_000) const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: { type: 'input', input: paymentRequest }, amount: optionalAmountSats, tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: undefined }) // If the fees are acceptable, continue to create the Send Payment if (prepareResponse.paymentMethod.type === 'bolt11Invoice') { // Fees to pay via Lightning const lightningFeeSats = prepareResponse.paymentMethod.lightningFeeSats // Or fees to pay (if available) via a Spark transfer const sparkTransferFeeSats = prepareResponse.paymentMethod.sparkTransferFeeSats console.debug(`Lightning Fees: ${lightningFeeSats} sats`) console.debug(`Spark Transfer Fees: ${sparkTransferFeeSats} sats`) } ``` ##### React Native ```typescript const paymentRequest = '' // Optionally set the amount you wish to pay the receiver const optionalAmountSats = BigInt(5_000) const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: new PaymentRequest.Input({ input: paymentRequest }), amount: optionalAmountSats, tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: undefined }) // If the fees are acceptable, continue to create the Send Payment if (prepareResponse.paymentMethod?.tag === SendPaymentMethod_Tags.Bolt11Invoice) { // Fees to pay via Lightning const lightningFeeSats = prepareResponse.paymentMethod.inner.lightningFeeSats // Or fees to pay (if available) via a Spark transfer const sparkTransferFeeSats = prepareResponse.paymentMethod.inner.sparkTransferFeeSats console.debug(`Lightning Fees: ${lightningFeeSats} sats`) console.debug(`Spark Transfer Fees: ${sparkTransferFeeSats} sats`) } ``` ##### Flutter ```dart String paymentRequest = ""; // Optionally set the amount you wish to pay the receiver BigInt? optionalAmountSats = BigInt.from(5000); final request = PrepareSendPaymentRequest( paymentRequest: PaymentRequest.input(input: paymentRequest), amount: optionalAmountSats, tokenIdentifier: null, conversionOptions: null, feePolicy: null); final response = await sdk.prepareSendPayment(request: request); // If the fees are acceptable, continue to create the Send Payment final paymentMethod = response.paymentMethod; if (paymentMethod is SendPaymentMethod_Bolt11Invoice) { // Fees to pay via Lightning final lightningFeeSats = paymentMethod.lightningFeeSats; // Or fees to pay (if available) via a Spark transfer final sparkTransferFeeSats = paymentMethod.sparkTransferFeeSats; print("Lightning Fees: $lightningFeeSats sats"); print("Spark Transfer Fees: $sparkTransferFeeSats sats"); } ``` ##### Python ```python payment_request = "" optional_amount_sats = 5_000 try: request = PrepareSendPaymentRequest( payment_request=PaymentRequest.INPUT(input=payment_request), amount=optional_amount_sats, token_identifier=None, conversion_options=None, fee_policy=None, ) prepare_response = await sdk.prepare_send_payment(request=request) # If the fees are acceptable, continue to create the Send Payment if isinstance( prepare_response.payment_method, SendPaymentMethod.BOLT11_INVOICE ): # Fees to pay via Lightning lightning_fee_sats = prepare_response.payment_method.lightning_fee_sats # Or fees to pay (if available) via a Spark transfer spark_transfer_fee_sats = ( prepare_response.payment_method.spark_transfer_fee_sats ) logging.debug(f"Lightning Fees: {lightning_fee_sats} sats") logging.debug(f"Spark Transfer Fees: {spark_transfer_fee_sats} sats") except Exception as error: logging.error(error) raise ``` ##### Go ```go paymentRequest := "" // Optionally set the amount you wish to pay the receiver optionalAmountSats := new(big.Int).SetInt64(5_000) request := breez_sdk_spark.PrepareSendPaymentRequest{ PaymentRequest: breez_sdk_spark.PaymentRequestInput{Input: paymentRequest}, Amount: &optionalAmountSats, TokenIdentifier: nil, ConversionOptions: nil, FeePolicy: nil, } response, err := sdk.PrepareSendPayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } // If the fees are acceptable, continue to create the Send Payment switch paymentMethod := response.PaymentMethod.(type) { case breez_sdk_spark.SendPaymentMethodBolt11Invoice: // Fees to pay via Lightning lightningFeeSats := paymentMethod.LightningFeeSats // Or fees to pay (if available) via a Spark transfer sparkTransferFeeSats := paymentMethod.SparkTransferFeeSats log.Printf("Lightning Fees: %v sats", lightningFeeSats) log.Printf("Spark Transfer Fees: %v sats", sparkTransferFeeSats) } ``` ### Bitcoin For Bitcoin addresses, the amount must be set in the request. The prepare response includes fee quotes for three payment speeds: Slow, Medium, and Fast. The quote's `is_estimate` flag is set when your bitcoin balance is zero and a token conversion will fund the payment, which happens automatically when [Stable Balance](/guide/stable_balance.md) is active. The fees shown are then derived from current on-chain rates rather than quoted by the provider, so the real ones may differ. They are an upper bound: the SDK fetches a real quote once the conversion completes, and fails rather than spending more than it estimated. Present this as an estimate if you show it. Fee quotes are short lived. If one is close to expiry by the time you send, the SDK fetches a fresh quote for you. It will not spend more than the payment allows, so a fee that rises beyond that fails the send and asks you to prepare again. #### Rust ```rust let payment_request = "".to_string(); // Set the amount you wish to pay the receiver let amount_sats = Some(50_000); let prepare_response = sdk .prepare_send_payment(PrepareSendPaymentRequest { payment_request: PaymentRequest::Input { input: payment_request, }, amount: amount_sats, token_identifier: None, conversion_options: None, fee_policy: None, }) .await?; // Review the fee quote for each confirmation speed if let SendPaymentMethod::BitcoinAddress { fee_quote, .. } = &prepare_response.payment_method { info!("Slow fee: {} sats", fee_quote.speed_slow.total_fee_sat()); info!( "Medium fee: {} sats", fee_quote.speed_medium.total_fee_sat() ); info!("Fast fee: {} sats", fee_quote.speed_fast.total_fee_sat()); } ``` #### Swift ```swift let paymentRequest = "" // Set the amount you wish to pay the receiver let amountSats = BInt(50_000) let prepareResponse = try await sdk.prepareSendPayment( request: PrepareSendPaymentRequest( paymentRequest: .input(input: paymentRequest), amount: amountSats, tokenIdentifier: nil, conversionOptions: nil, feePolicy: nil )) // Review the fee quote for each confirmation speed if case let .bitcoinAddress(address: _, feeQuote: feeQuote) = prepareResponse.paymentMethod { let slowFeeSats = feeQuote.speedSlow.userFeeSat + feeQuote.speedSlow.l1BroadcastFeeSat let mediumFeeSats = feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat let fastFeeSats = feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat print("Slow fee: \(slowFeeSats) sats") print("Medium fee: \(mediumFeeSats) sats") print("Fast fee: \(fastFeeSats) sats") } ``` #### Kotlin ```kotlin val paymentRequest = "" // Set the amount you wish to pay the receiver // Kotlin MPP (BigInteger from com.ionspin.kotlin.bignum.integer) val amountSats = BigInteger.fromLong(50_000L) // Android (BigInteger from java.math) // val amountSats = BigInteger.valueOf(50_000L) try { val req = PrepareSendPaymentRequest( paymentRequest = PaymentRequest.Input(input = paymentRequest), amount = amountSats, tokenIdentifier = null, conversionOptions = null, feePolicy = null, ) val prepareResponse = sdk.prepareSendPayment(req) // Review the fee quote for each confirmation speed val paymentMethod = prepareResponse.paymentMethod if (paymentMethod is SendPaymentMethod.BitcoinAddress) { val feeQuote = paymentMethod.feeQuote val slowFeeSats = feeQuote.speedSlow.userFeeSat + feeQuote.speedSlow.l1BroadcastFeeSat val mediumFeeSats = feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat val fastFeeSats = feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat // Log.v("Breez", "Slow fee: $slowFeeSats sats") // Log.v("Breez", "Medium fee: $mediumFeeSats sats") // Log.v("Breez", "Fast fee: $fastFeeSats sats") } } catch (e: Exception) { // handle error } ``` #### C# ```csharp var paymentRequest = ""; // Set the amount you wish to pay the receiver ulong? amountSats = 50_000UL; var request = new PrepareSendPaymentRequest( paymentRequest: new PaymentRequest.Input(input: paymentRequest), amount: amountSats, tokenIdentifier: null, conversionOptions: null, feePolicy: null ); var prepareResponse = await sdk.PrepareSendPayment(request: request); // Review the fee quote for each confirmation speed if (prepareResponse.paymentMethod is SendPaymentMethod.BitcoinAddress bitcoinMethod) { var feeQuote = bitcoinMethod.feeQuote; var slowFeeSats = feeQuote.speedSlow.userFeeSat + feeQuote.speedSlow.l1BroadcastFeeSat; var mediumFeeSats = feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat; var fastFeeSats = feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat; Console.WriteLine($"Slow fee: {slowFeeSats} sats"); Console.WriteLine($"Medium fee: {mediumFeeSats} sats"); Console.WriteLine($"Fast fee: {fastFeeSats} sats"); } ``` #### Javascript (Wasm) ```typescript const paymentRequest = '' // Set the amount you wish to pay the receiver const amountSats = BigInt(50_000) const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: { type: 'input', input: paymentRequest }, amount: amountSats, tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: undefined }) // Review the fee quote for each confirmation speed if (prepareResponse.paymentMethod.type === 'bitcoinAddress') { const feeQuote = prepareResponse.paymentMethod.feeQuote const slowFeeSats = feeQuote.speedSlow.userFeeSat + feeQuote.speedSlow.l1BroadcastFeeSat const mediumFeeSats = feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat const fastFeeSats = feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat console.debug(`Slow fee: ${slowFeeSats} sats`) console.debug(`Medium fee: ${mediumFeeSats} sats`) console.debug(`Fast fee: ${fastFeeSats} sats`) } ``` #### React Native ```typescript const paymentRequest = '' // Set the amount you wish to pay the receiver const amountSats = BigInt(50_000) const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: new PaymentRequest.Input({ input: paymentRequest }), amount: amountSats, tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: undefined }) // Review the fee quote for each confirmation speed if (prepareResponse.paymentMethod?.tag === SendPaymentMethod_Tags.BitcoinAddress) { const feeQuote = prepareResponse.paymentMethod.inner.feeQuote const slowFeeSats = feeQuote.speedSlow.userFeeSat + feeQuote.speedSlow.l1BroadcastFeeSat const mediumFeeSats = feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat const fastFeeSats = feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat console.debug(`Slow fee: ${slowFeeSats} sats`) console.debug(`Medium fee: ${mediumFeeSats} sats`) console.debug(`Fast fee: ${fastFeeSats} sats`) } ``` #### Flutter ```dart String paymentRequest = ""; // Set the amount you wish to pay the receiver BigInt? amountSats = BigInt.from(50000); final request = PrepareSendPaymentRequest( paymentRequest: PaymentRequest.input(input: paymentRequest), amount: amountSats, tokenIdentifier: null, conversionOptions: null, feePolicy: null); final response = await sdk.prepareSendPayment(request: request); // Review the fee quote for each confirmation speed final paymentMethod = response.paymentMethod; if (paymentMethod is SendPaymentMethod_BitcoinAddress) { final feeQuote = paymentMethod.feeQuote; final slowFeeSats = feeQuote.speedSlow.userFeeSat + feeQuote.speedSlow.l1BroadcastFeeSat; final mediumFeeSats = feeQuote.speedMedium.userFeeSat + feeQuote.speedMedium.l1BroadcastFeeSat; final fastFeeSats = feeQuote.speedFast.userFeeSat + feeQuote.speedFast.l1BroadcastFeeSat; print("Slow fee: $slowFeeSats sats"); print("Medium fee: $mediumFeeSats sats"); print("Fast fee: $fastFeeSats sats"); } ``` #### Python ```python payment_request = "" amount_sats = 50_000 try: request = PrepareSendPaymentRequest( payment_request=PaymentRequest.INPUT(input=payment_request), amount=amount_sats, token_identifier=None, conversion_options=None, fee_policy=None, ) prepare_response = await sdk.prepare_send_payment(request=request) # Review the fee quote for each confirmation speed if isinstance( prepare_response.payment_method, SendPaymentMethod.BITCOIN_ADDRESS ): fee_quote = prepare_response.payment_method.fee_quote slow_fee_sats = ( fee_quote.speed_slow.user_fee_sat + fee_quote.speed_slow.l1_broadcast_fee_sat ) medium_fee_sats = ( fee_quote.speed_medium.user_fee_sat + fee_quote.speed_medium.l1_broadcast_fee_sat ) fast_fee_sats = ( fee_quote.speed_fast.user_fee_sat + fee_quote.speed_fast.l1_broadcast_fee_sat ) logging.debug(f"Slow fee: {slow_fee_sats} sats") logging.debug(f"Medium fee: {medium_fee_sats} sats") logging.debug(f"Fast fee: {fast_fee_sats} sats") except Exception as error: logging.error(error) raise ``` #### Go ```go paymentRequest := "" // Set the amount you wish to pay the receiver amountSats := new(big.Int).SetInt64(50_000) request := breez_sdk_spark.PrepareSendPaymentRequest{ PaymentRequest: breez_sdk_spark.PaymentRequestInput{Input: paymentRequest}, Amount: &amountSats, TokenIdentifier: nil, ConversionOptions: nil, FeePolicy: nil, } response, err := sdk.PrepareSendPayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } // Review the fee quote for each confirmation speed switch paymentMethod := response.PaymentMethod.(type) { case breez_sdk_spark.SendPaymentMethodBitcoinAddress: feeQuote := paymentMethod.FeeQuote slowFeeSats := feeQuote.SpeedSlow.UserFeeSat + feeQuote.SpeedSlow.L1BroadcastFeeSat mediumFeeSats := feeQuote.SpeedMedium.UserFeeSat + feeQuote.SpeedMedium.L1BroadcastFeeSat fastFeeSats := feeQuote.SpeedFast.UserFeeSat + feeQuote.SpeedFast.L1BroadcastFeeSat log.Printf("Slow fee: %v sats", slowFeeSats) log.Printf("Medium fee: %v sats", mediumFeeSats) log.Printf("Fast fee: %v sats", fastFeeSats) } ``` ### Spark #### Spark address For Spark addresses, the amount must be set in the request. Sending to a Spark address uses a direct Spark transfer. ##### Rust ```rust let payment_request = "".to_string(); // Set the amount you wish to pay the receiver let amount_sats = Some(50_000); let prepare_response = sdk .prepare_send_payment(PrepareSendPaymentRequest { payment_request: PaymentRequest::Input { input: payment_request, }, amount: amount_sats, token_identifier: None, conversion_options: None, fee_policy: None, }) .await?; // If the fees are acceptable, continue to create the Send Payment if let SendPaymentMethod::SparkAddress { fee, .. } = prepare_response.payment_method { info!("Fees: {} sats", fee); } ``` ##### Swift ```swift let paymentRequest = "" // Set the amount you wish to pay the receiver let amountSats = BInt(50_000) let prepareResponse = try await sdk.prepareSendPayment( request: PrepareSendPaymentRequest( paymentRequest: .input(input: paymentRequest), amount: amountSats, tokenIdentifier: nil, conversionOptions: nil, feePolicy: nil )) if case let .sparkAddress(_, feeSats, _) = prepareResponse.paymentMethod { print("Fees: \(feeSats) sats") } ``` ##### Kotlin ```kotlin val paymentRequest = "" // Set the amount you wish to pay the receiver // Kotlin MPP (BigInteger from com.ionspin.kotlin.bignum.integer) val amountSats = BigInteger.fromLong(50_000L) // Android (BigInteger from java.math) // val amountSats = BigInteger.valueOf(50_000L) try { val req = PrepareSendPaymentRequest( paymentRequest = PaymentRequest.Input(input = paymentRequest), amount = amountSats, tokenIdentifier = null, conversionOptions = null, feePolicy = null, ) val prepareResponse = sdk.prepareSendPayment(req) // If the fees are acceptable, continue to create the Send Payment val paymentMethod = prepareResponse.paymentMethod if (paymentMethod is SendPaymentMethod.SparkAddress) { val feeSats = paymentMethod.fee // Log.v("Breez", "Fees: ${feeSats} sats") } } catch (e: Exception) { // handle error } ``` ##### C# ```csharp var paymentRequest = ""; // Set the amount you wish to pay the receiver ulong? amountSats = 50_000UL; var request = new PrepareSendPaymentRequest( paymentRequest: new PaymentRequest.Input(input: paymentRequest), amount: amountSats, tokenIdentifier: null, conversionOptions: null, feePolicy: null ); var prepareResponse = await sdk.PrepareSendPayment(request: request); // If the fees are acceptable, continue to create the Send Payment if (prepareResponse.paymentMethod is SendPaymentMethod.SparkAddress sparkMethod) { var fee = sparkMethod.fee; Console.WriteLine($"Fees: {fee} sats"); } ``` ##### Javascript (Wasm) ```typescript const paymentRequest = '' // Set the amount you wish to pay the receiver const amountSats = BigInt(50_000) const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: { type: 'input', input: paymentRequest }, amount: amountSats, tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: undefined }) // If the fees are acceptable, continue to create the Send Payment if (prepareResponse.paymentMethod.type === 'sparkAddress') { const feeSats = prepareResponse.paymentMethod.fee console.debug(`Fees: ${feeSats} sats`) } ``` ##### React Native ```typescript const paymentRequest = '' // Set the amount you wish to pay the receiver const amountSats = BigInt(50_000) const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: new PaymentRequest.Input({ input: paymentRequest }), amount: amountSats, tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: undefined }) // If the fees are acceptable, continue to create the Send Payment if (prepareResponse.paymentMethod?.tag === SendPaymentMethod_Tags.SparkAddress) { const feeSats = prepareResponse.paymentMethod.inner.fee console.debug(`Fees: ${feeSats} sats`) } ``` ##### Flutter ```dart String paymentRequest = ""; // Set the amount you wish to pay the receiver BigInt? amountSats = BigInt.from(50000); final request = PrepareSendPaymentRequest( paymentRequest: PaymentRequest.input(input: paymentRequest), amount: amountSats, tokenIdentifier: null, conversionOptions: null, feePolicy: null); final response = await sdk.prepareSendPayment(request: request); // If the fees are acceptable, continue to create the Send Payment final paymentMethod = response.paymentMethod; if (paymentMethod is SendPaymentMethod_SparkAddress) { final feeSats = paymentMethod.fee; print("Fees: $feeSats sats"); } ``` ##### Python ```python payment_request = "" amount_sats = 50_000 try: request = PrepareSendPaymentRequest( payment_request=PaymentRequest.INPUT(input=payment_request), amount=amount_sats, token_identifier=None, conversion_options=None, fee_policy=None, ) prepare_response = await sdk.prepare_send_payment(request=request) # If the fees are acceptable, continue to create the Send Payment if isinstance(prepare_response.payment_method, SendPaymentMethod.SPARK_ADDRESS): fee = prepare_response.payment_method.fee logging.debug(f"Fees: {fee} sats") except Exception as error: logging.error(error) raise ``` ##### Go ```go paymentRequest := "" // Set the amount you wish to pay the receiver amountSats := new(big.Int).SetInt64(50_000) request := breez_sdk_spark.PrepareSendPaymentRequest{ PaymentRequest: breez_sdk_spark.PaymentRequestInput{Input: paymentRequest}, Amount: &amountSats, TokenIdentifier: nil, ConversionOptions: nil, FeePolicy: nil, } response, err := sdk.PrepareSendPayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } // If the fees are acceptable, continue to create the Send Payment switch paymentMethod := response.PaymentMethod.(type) { case breez_sdk_spark.SendPaymentMethodSparkAddress: feeSats := paymentMethod.Fee log.Printf("Fees: %v sats", feeSats) } ``` #### Spark invoice For Spark invoices, the amount can be optionally set. It is only required if the invoice doesn't specify an amount. If the invoice specifies an amount, providing a different amount is not supported. **Developer note** Spark invoices may require a token (non-Bitcoin) as the payment asset. To determine the requirements of a Spark invoice and any restrictions it may impose, see the Parsing inputs page. To learn more about tokens, see the Handling tokens page. ##### Rust ```rust let payment_request = "".to_string(); // Optionally set the amount you wish to pay the receiver let optional_amount_sats = Some(50_000); let prepare_response = sdk .prepare_send_payment(PrepareSendPaymentRequest { payment_request: PaymentRequest::Input { input: payment_request, }, amount: optional_amount_sats, token_identifier: None, conversion_options: None, fee_policy: None, }) .await?; // If the fees are acceptable, continue to create the Send Payment if let SendPaymentMethod::SparkInvoice { fee, .. } = prepare_response.payment_method { info!("Fees: {} sats", fee); } ``` ##### Swift ```swift let paymentRequest = "" // Optionally set the amount you wish to pay the receiver let optionalAmountSats: BInt? = BInt(50_000) let prepareResponse = try await sdk.prepareSendPayment( request: PrepareSendPaymentRequest( paymentRequest: .input(input: paymentRequest), amount: optionalAmountSats, tokenIdentifier: nil, conversionOptions: nil, feePolicy: nil )) if case let .sparkInvoice(_, feeSats, _) = prepareResponse.paymentMethod { print("Fees: \(feeSats) sats") } ``` ##### Kotlin ```kotlin val paymentRequest = "" // Optionally set the amount you wish to pay the receiver // Kotlin MPP (BigInteger from com.ionspin.kotlin.bignum.integer) val optionalAmountSats = BigInteger.fromLong(50_000L) // Android (BigInteger from java.math) // val optionalAmountSats = BigInteger.valueOf(50_000L) try { val req = PrepareSendPaymentRequest( paymentRequest = PaymentRequest.Input(input = paymentRequest), amount = optionalAmountSats, tokenIdentifier = null, conversionOptions = null, feePolicy = null, ) val prepareResponse = sdk.prepareSendPayment(req) // If the fees are acceptable, continue to create the Send Payment val paymentMethod = prepareResponse.paymentMethod if (paymentMethod is SendPaymentMethod.SparkInvoice) { val feeSats = paymentMethod.fee // Log.v("Breez", "Fees: ${feeSats} sats") } } catch (e: Exception) { // handle error } ``` ##### C# ```csharp var paymentRequest = ""; // Optionally set the amount you wish to pay the receiver ulong? optionalAmountSats = 50_000UL; var request = new PrepareSendPaymentRequest( paymentRequest: new PaymentRequest.Input(input: paymentRequest), amount: optionalAmountSats, tokenIdentifier: null, conversionOptions: null, feePolicy: null ); var prepareResponse = await sdk.PrepareSendPayment(request: request); // If the fees are acceptable, continue to create the Send Payment if (prepareResponse.paymentMethod is SendPaymentMethod.SparkInvoice sparkInvoiceMethod) { var fee = sparkInvoiceMethod.fee; Console.WriteLine($"Fees: {fee} sats"); } ``` ##### Javascript (Wasm) ```typescript const paymentRequest = '' // Optionally set the amount you wish to pay the receiver const optionalAmountSats = BigInt(50_000) const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: { type: 'input', input: paymentRequest }, amount: optionalAmountSats, tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: undefined }) // If the fees are acceptable, continue to create the Send Payment if (prepareResponse.paymentMethod.type === 'sparkInvoice') { const feeSats = prepareResponse.paymentMethod.fee console.debug(`Fees: ${feeSats} sats`) } ``` ##### React Native ```typescript const paymentRequest = '' // Optionally set the amount you wish to pay the receiver const optionalAmountSats = BigInt(50_000) const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: new PaymentRequest.Input({ input: paymentRequest }), amount: optionalAmountSats, tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: undefined }) // If the fees are acceptable, continue to create the Send Payment if (prepareResponse.paymentMethod?.tag === SendPaymentMethod_Tags.SparkInvoice) { const feeSats = prepareResponse.paymentMethod.inner.fee console.debug(`Fees: ${feeSats} sats`) } ``` ##### Flutter ```dart String paymentRequest = ""; // Optionally set the amount you wish to pay the receiver BigInt? optionalAmountSats = BigInt.from(50000); final request = PrepareSendPaymentRequest( paymentRequest: PaymentRequest.input(input: paymentRequest), amount: optionalAmountSats, tokenIdentifier: null, conversionOptions: null, feePolicy: null); final response = await sdk.prepareSendPayment(request: request); // If the fees are acceptable, continue to create the Send Payment final paymentMethod = response.paymentMethod; if (paymentMethod is SendPaymentMethod_SparkInvoice) { final feeSats = paymentMethod.fee; print("Fees: $feeSats sats"); } ``` ##### Python ```python payment_request = "" optional_amount_sats = 50_000 try: request = PrepareSendPaymentRequest( payment_request=PaymentRequest.INPUT(input=payment_request), amount=optional_amount_sats, token_identifier=None, conversion_options=None, fee_policy=None, ) prepare_response = await sdk.prepare_send_payment(request=request) # If the fees are acceptable, continue to create the Send Payment if isinstance(prepare_response.payment_method, SendPaymentMethod.SPARK_INVOICE): fee = prepare_response.payment_method.fee logging.debug(f"Fees: {fee} sats") except Exception as error: logging.error(error) raise ``` ##### Go ```go paymentRequest := "" // Optionally set the amount you wish to pay the receiver optionalAmountSats := new(big.Int).SetInt64(50_000) request := breez_sdk_spark.PrepareSendPaymentRequest{ PaymentRequest: breez_sdk_spark.PaymentRequestInput{Input: paymentRequest}, Amount: &optionalAmountSats, TokenIdentifier: nil, ConversionOptions: nil, FeePolicy: nil, } response, err := sdk.PrepareSendPayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } // If the fees are acceptable, continue to create the Send Payment switch paymentMethod := response.PaymentMethod.(type) { case breez_sdk_spark.SendPaymentMethodSparkInvoice: feeSats := paymentMethod.Fee log.Printf("Fees: %v sats", feeSats) } ``` ### USDC/USDT Send USDC or USDT from a Spark wallet to a recipient on one of several supported chains: Ethereum-family chains (Arbitrum, Base, and similar EVM networks), Solana, and Tron. The source on the Spark side is BTC sats or [USDB](/guide/stable_balance.md) (a 6-decimal USD-pegged token on Spark). This feature must be enabled in [the SDK configuration](/guide/config.md#usdc-usdt) before using. See [USDC/USDT](/guide/cross_chain.md) for provider details and the status lifecycle. After [parsing](/guide/parse.md) the recipient address into `InputType::CrossChainAddress`, call `get_cross_chain_routes` with `CrossChainRouteFilter::Send` carrying the parsed `CrossChainAddressDetails`. The returned `CrossChainRoutePair`s name the provider, destination chain and asset, decimals, optional token contract address, and which source assets (BTC sats or USDB) each route accepts. #### Rust ```rust let input = ""; let InputType::CrossChainAddress(address_details) = sdk.parse(input).await? else { anyhow::bail!("Not a cross-chain address"); }; let routes = sdk .get_cross_chain_routes(&CrossChainRouteFilter::Send { address_details: address_details.clone(), }) .await?; for route in &routes { info!( "Route via {:?}: {}/{}", route.provider, route.chain, route.asset ); } ``` #### Swift ```swift let input = "" let parsed = try await sdk.parse(input: input) guard case let .crossChainAddress(v1: addressDetails) = parsed else { throw NSError(domain: "CrossChain", code: 1) } let routes = try await sdk.getCrossChainRoutes( filter: .send(addressDetails: addressDetails)) for route in routes { print("Route via \(route.provider): \(route.chain)/\(route.asset)") } ``` #### Kotlin ```kotlin val input = "" try { val parsed = sdk.parse(input) if (parsed !is InputType.CrossChainAddress) { throw IllegalArgumentException("Not a cross-chain address") } val addressDetails = parsed.v1 val routes = sdk.getCrossChainRoutes( CrossChainRouteFilter.Send(addressDetails = addressDetails) ) for (route in routes) { // Log.v("Breez", "Route via ${route.provider}: ${route.chain}/${route.asset}") } } catch (e: Exception) { // handle error } ``` #### C# ```csharp var inputStr = ""; var parsed = await sdk.Parse(input: inputStr); if (parsed is not InputType.CrossChainAddress crossChain) { throw new InvalidOperationException("Not a cross-chain address"); } var addressDetails = crossChain.v1; var filter = new CrossChainRouteFilter.Send(addressDetails: addressDetails); var routes = await sdk.GetCrossChainRoutes(filter: filter); foreach (var route in routes) { Console.WriteLine($"Route via {route.provider}: {route.chain}/{route.asset}"); } ``` #### Javascript (Wasm) ```typescript const input = '' const parsed = await sdk.parse(input) if (parsed.type !== 'crossChainAddress') { throw new Error('Not a cross-chain address') } const routes = await sdk.getCrossChainRoutes({ type: 'send', addressDetails: parsed }) for (const route of routes) { console.debug(`Route via ${route.provider}: ${route.chain}/${route.asset}`) } ``` #### React Native ```typescript const input = '' const parsed = await sdk.parse(input) if (parsed.tag !== InputType_Tags.CrossChainAddress) { throw new Error('Not a cross-chain address') } const addressDetails = parsed.inner[0] const routes = await sdk.getCrossChainRoutes( new CrossChainRouteFilter.Send({ addressDetails }) ) for (const route of routes) { console.debug(`Route via ${route.provider}: ${route.chain}/${route.asset}`) } ``` #### Flutter ```dart String input = ""; InputType parsed = await sdk.parse(input: input); if (parsed is! InputType_CrossChainAddress) { throw Exception("Not a cross-chain address"); } CrossChainAddressDetails addressDetails = parsed.field0; List routes = await sdk.getCrossChainRoutes( filter: CrossChainRouteFilter.send(addressDetails: addressDetails), ); for (var route in routes) { print("Route via ${route.provider}: ${route.chain}/${route.asset}"); } ``` #### Python ```python input_str = "" try: parsed = await sdk.parse(input=input_str) if not isinstance(parsed, InputType.CROSS_CHAIN_ADDRESS): raise ValueError("Not a cross-chain address") address_details = parsed[0] routes = await sdk.get_cross_chain_routes( filter=CrossChainRouteFilter.SEND(address_details=address_details) ) for route in routes: logging.debug( f"Route via {route.provider}: {route.chain}/{route.asset}" ) except Exception as error: logging.error(error) raise ``` #### Go ```go inputStr := "" input, err := sdk.Parse(inputStr) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError } return nil, err } addressInput, ok := input.(breez_sdk_spark.InputTypeCrossChainAddress) if !ok { return nil, errors.New("not a cross-chain address") } addressDetails := addressInput.Field0 filter := breez_sdk_spark.CrossChainRouteFilterSend{AddressDetails: addressDetails} routes, err := sdk.GetCrossChainRoutes(filter) if err != nil { return nil, err } for _, route := range routes { log.Printf("Route via %v: %s/%s", route.Provider, route.Chain, route.Asset) } ``` Build `PaymentRequest::CrossChain` with the recipient address, the chosen route, and an optional `max_slippage_bps` (10 to 500 basis points). The amount on the prepare request is denominated in the source asset's base units: sats for a BTC source, USDB base units for a USDB source. The prepare response carries a quote `expires_at` timestamp. Re-prepare and pick a fresh route if it lapses before send. #### Rust ```rust // Optionally set the maximum slippage in basis points (10 to 500) let optional_max_slippage_bps = Some(100); let prepare_response = sdk .prepare_send_payment(PrepareSendPaymentRequest { payment_request: PaymentRequest::CrossChain { address: address_details.address.clone(), route, max_slippage_bps: optional_max_slippage_bps, target_overpay_bps: None, }, amount: Some(50_000), token_identifier: None, conversion_options: None, fee_policy: None, }) .await?; if let SendPaymentMethod::CrossChainAddress { amount_in, estimated_out, fee_amount, expires_at, .. } = &prepare_response.payment_method { info!("Amount in: {amount_in}"); info!("Estimated out: {estimated_out}"); info!("Provider fee: {fee_amount}"); info!("Quote expires at: {expires_at}"); } ``` #### Swift ```swift // Optionally set the maximum slippage in basis points (10 to 500) let optionalMaxSlippageBps: UInt32? = 100 let prepareResponse = try await sdk.prepareSendPayment( request: PrepareSendPaymentRequest( paymentRequest: .crossChain( address: addressDetails.address, route: route, maxSlippageBps: optionalMaxSlippageBps, targetOverpayBps: nil ), amount: BInt(50_000), tokenIdentifier: nil, conversionOptions: nil, feePolicy: nil )) if case let .crossChainAddress( _, _, amountIn, _, estimatedOut, feeAmount, _, _, _, _, expiresAt, _ ) = prepareResponse.paymentMethod { print("Amount in: \(amountIn)") print("Estimated out: \(estimatedOut)") print("Provider fee: \(feeAmount)") print("Quote expires at: \(expiresAt)") } ``` #### Kotlin ```kotlin // Optionally set the maximum slippage in basis points (10 to 500) val optionalMaxSlippageBps: UInt? = 100u try { val req = PrepareSendPaymentRequest( paymentRequest = PaymentRequest.CrossChain( address = addressDetails.address, route = route, maxSlippageBps = optionalMaxSlippageBps, targetOverpayBps = null, ), amount = BigInteger.fromLong(50_000L), tokenIdentifier = null, conversionOptions = null, feePolicy = null, ) val prepareResponse = sdk.prepareSendPayment(req) val paymentMethod = prepareResponse.paymentMethod if (paymentMethod is SendPaymentMethod.CrossChainAddress) { val amountIn = paymentMethod.amountIn val estimatedOut = paymentMethod.estimatedOut val feeAmount = paymentMethod.feeAmount val expiresAt = paymentMethod.expiresAt // Log.v("Breez", "Amount in: $amountIn") // Log.v("Breez", "Estimated out: $estimatedOut") // Log.v("Breez", "Provider fee: $feeAmount") // Log.v("Breez", "Quote expires at: $expiresAt") } } catch (e: Exception) { // handle error } ``` #### C# ```csharp // Optionally set the maximum slippage in basis points (10 to 500) uint? optionalMaxSlippageBps = 100; var request = new PrepareSendPaymentRequest( paymentRequest: new PaymentRequest.CrossChain( address: addressDetails.address, route: route, maxSlippageBps: optionalMaxSlippageBps, targetOverpayBps: null ), amount: 50_000UL, tokenIdentifier: null, conversionOptions: null, feePolicy: null ); var prepareResponse = await sdk.PrepareSendPayment(request: request); if (prepareResponse.paymentMethod is SendPaymentMethod.CrossChainAddress method) { Console.WriteLine($"Amount in: {method.amountIn}"); Console.WriteLine($"Estimated out: {method.estimatedOut}"); Console.WriteLine($"Provider fee: {method.feeAmount}"); Console.WriteLine($"Quote expires at: {method.expiresAt}"); } ``` #### Javascript (Wasm) ```typescript // Optionally set the maximum slippage in basis points (10 to 500) const optionalMaxSlippageBps = 100 const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: { type: 'crossChain', address: addressDetails.address, route, maxSlippageBps: optionalMaxSlippageBps }, amount: BigInt(50_000), tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: undefined }) if (prepareResponse.paymentMethod.type === 'crossChainAddress') { const { amountIn, estimatedOut, feeAmount, expiresAt } = prepareResponse.paymentMethod console.debug(`Amount in: ${amountIn}`) console.debug(`Estimated out: ${estimatedOut}`) console.debug(`Provider fee: ${feeAmount}`) console.debug(`Quote expires at: ${expiresAt}`) } ``` #### React Native ```typescript // Optionally set the maximum slippage in basis points (10 to 500) const optionalMaxSlippageBps = 100 const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: new PaymentRequest.CrossChain({ address: addressDetails.address, route, maxSlippageBps: optionalMaxSlippageBps, targetOverpayBps: undefined }), amount: BigInt(50_000), tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: undefined }) if (prepareResponse.paymentMethod?.tag === SendPaymentMethod_Tags.CrossChainAddress) { const inner = prepareResponse.paymentMethod.inner console.debug(`Amount in: ${inner.amountIn}`) console.debug(`Estimated out: ${inner.estimatedOut}`) console.debug(`Provider fee: ${inner.feeAmount}`) console.debug(`Quote expires at: ${inner.expiresAt}`) } ``` #### Flutter ```dart // Optionally set the maximum slippage in basis points (10 to 500) int? optionalMaxSlippageBps = 100; final request = PrepareSendPaymentRequest( paymentRequest: PaymentRequest.crossChain( address: addressDetails.address, route: route, maxSlippageBps: optionalMaxSlippageBps, targetOverpayBps: null, ), amount: BigInt.from(50000), tokenIdentifier: null, conversionOptions: null, feePolicy: null, ); final response = await sdk.prepareSendPayment(request: request); final paymentMethod = response.paymentMethod; if (paymentMethod is SendPaymentMethod_CrossChainAddress) { print("Amount in: ${paymentMethod.amountIn}"); print("Estimated out: ${paymentMethod.estimatedOut}"); print("Provider fee: ${paymentMethod.feeAmount}"); print("Quote expires at: ${paymentMethod.expiresAt}"); } ``` #### Python ```python # Optionally set the maximum slippage in basis points (10 to 500) optional_max_slippage_bps = 100 try: request = PrepareSendPaymentRequest( payment_request=PaymentRequest.CROSS_CHAIN( address=address_details.address, route=route, max_slippage_bps=optional_max_slippage_bps, target_overpay_bps=None, ), amount=50_000, token_identifier=None, conversion_options=None, fee_policy=None, ) prepare_response = await sdk.prepare_send_payment(request=request) if isinstance( prepare_response.payment_method, SendPaymentMethod.CROSS_CHAIN_ADDRESS ): method = prepare_response.payment_method logging.debug(f"Amount in: {method.amount_in}") logging.debug(f"Estimated out: {method.estimated_out}") logging.debug(f"Provider fee: {method.fee_amount}") logging.debug(f"Quote expires at: {method.expires_at}") except Exception as error: logging.error(error) raise ``` #### Go ```go // Optionally set the maximum slippage in basis points (10 to 500) optionalMaxSlippageBps := uint32(100) amount := new(big.Int).SetInt64(50_000) request := breez_sdk_spark.PrepareSendPaymentRequest{ PaymentRequest: breez_sdk_spark.PaymentRequestCrossChain{ Address: addressDetails.Address, Route: route, MaxSlippageBps: &optionalMaxSlippageBps, TargetOverpayBps: nil, }, Amount: &amount, TokenIdentifier: nil, ConversionOptions: nil, FeePolicy: nil, } response, err := sdk.PrepareSendPayment(request) if err != nil { return nil, err } switch paymentMethod := response.PaymentMethod.(type) { case breez_sdk_spark.SendPaymentMethodCrossChainAddress: log.Printf("Amount in: %v", paymentMethod.AmountIn) log.Printf("Estimated out: %v", paymentMethod.EstimatedOut) log.Printf("Provider fee: %v", paymentMethod.FeeAmount) log.Printf("Quote expires at: %s", paymentMethod.ExpiresAt) } ``` ## Fee Policy By default, fees are added on top of the amount (`FeePolicy::FeesExcluded`). Use `FeePolicy::FeesIncluded` to deduct fees from the amount instead—the receiver gets the amount minus fees. This is particularly useful when you want to spend your entire balance in a single payment—simply provide your full balance as the amount. Note: `FeePolicy::FeesIncluded` is not compatible with payment requests that specify an amount (e.g., BOLT11 invoices and Spark invoices with amount). ### Rust ```rust // By default (FeePolicy::FeesExcluded), fees are added on top of the amount. // Use FeePolicy::FeesIncluded to deduct fees from the amount instead. // The receiver gets amount minus fees. let payment_request = "".to_string(); let amount_sats = Some(50_000); let prepare_response = sdk .prepare_send_payment(PrepareSendPaymentRequest { payment_request: PaymentRequest::Input { input: payment_request, }, amount: amount_sats, token_identifier: None, conversion_options: None, fee_policy: Some(FeePolicy::FeesIncluded), }) .await?; // The response shows the fee policy used info!("Fee policy: {:?}", prepare_response.fee_policy); info!("Amount: {}", prepare_response.amount); // The receiver gets amount - fees (fees are available in prepare_response.payment_method) ``` ### Swift ```swift // By default (.feesExcluded), fees are added on top of the amount. // Use .feesIncluded to deduct fees from the amount instead. // The receiver gets amount minus fees. let paymentRequest = "" let amountSats: BInt? = BInt(50_000) let prepareResponse = try await sdk.prepareSendPayment( request: PrepareSendPaymentRequest( paymentRequest: .input(input: paymentRequest), amount: amountSats, tokenIdentifier: nil, conversionOptions: nil, feePolicy: .feesIncluded )) // The response shows the fee policy used print("Fee policy: \(String(describing: prepareResponse.feePolicy))") print("Amount: \(String(describing: prepareResponse.amount))") // The receiver gets amount - fees (fees are available in prepareResponse.paymentMethod) ``` ### Kotlin ```kotlin // By default (FeePolicy.FEES_EXCLUDED), fees are added on top of the amount. // Use FeePolicy.FEES_INCLUDED to deduct fees from the amount instead. // The receiver gets amount minus fees. val paymentRequest = "" // Kotlin MPP (BigInteger from com.ionspin.kotlin.bignum.integer) val amountSats = BigInteger.fromLong(50_000L) // Android (BigInteger from java.math) // val amountSats = BigInteger.valueOf(50_000L) try { val req = PrepareSendPaymentRequest( paymentRequest = PaymentRequest.Input(input = paymentRequest), amount = amountSats, tokenIdentifier = null, conversionOptions = null, feePolicy = FeePolicy.FEES_INCLUDED, ) val prepareResponse = sdk.prepareSendPayment(req) // The response shows the fee policy used // Log.v("Breez", "Fee policy: ${prepareResponse.feePolicy}") // Log.v("Breez", "Amount: ${prepareResponse.amount}") // The receiver gets amount - fees (fees are available in prepareResponse.paymentMethod) } catch (e: Exception) { // handle error } ``` ### C# ```csharp // By default (FeePolicy.FeesExcluded), fees are added on top of the amount. // Use FeePolicy.FeesIncluded to deduct fees from the amount instead. // The receiver gets amount minus fees. var paymentRequest = ""; ulong? amountSats = 50_000UL; var request = new PrepareSendPaymentRequest( paymentRequest: new PaymentRequest.Input(input: paymentRequest), amount: amountSats, tokenIdentifier: null, conversionOptions: null, feePolicy: FeePolicy.FeesIncluded ); var prepareResponse = await sdk.PrepareSendPayment(request: request); // The response shows the fee policy used Console.WriteLine($"Fee policy: {prepareResponse.feePolicy}"); Console.WriteLine($"Amount: {prepareResponse.amount}"); // The receiver gets amount - fees (fees are available in prepareResponse.paymentMethod) ``` ### Javascript (Wasm) ```typescript // By default ({ type: 'feesExcluded' }), fees are added on top of the amount. // Use { type: 'feesIncluded' } to deduct fees from the amount instead. // The receiver gets amount minus fees. const paymentRequest = '' const amountSats = BigInt(50_000) const feePolicy: FeePolicy = 'feesIncluded' const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: { type: 'input', input: paymentRequest }, amount: amountSats, tokenIdentifier: undefined, conversionOptions: undefined, feePolicy }) // The response shows the fee policy used console.log(`Fee policy: ${JSON.stringify(prepareResponse.feePolicy)}`) console.log(`Amount: ${prepareResponse.amount}`) // The receiver gets amount - fees (fees are available in prepareResponse.paymentMethod) ``` ### React Native ```typescript // By default (FeePolicy.FeesExcluded), fees are added on top of the amount. // Use FeePolicy.FeesIncluded to deduct fees from the amount instead. // The receiver gets amount minus fees. const paymentRequest = '' const amountSats = BigInt(50_000) const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: new PaymentRequest.Input({ input: paymentRequest }), amount: amountSats, tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: FeePolicy.FeesIncluded }) // The response shows the fee policy used console.log(`Fee policy: ${JSON.stringify(prepareResponse.feePolicy)}`) console.log(`Amount: ${prepareResponse.amount}`) // The receiver gets amount - fees (fees are available in prepareResponse.paymentMethod) ``` ### Flutter ```dart // By default (FeePolicy.feesExcluded), fees are added on top of the amount. // Use FeePolicy.feesIncluded to deduct fees from the amount instead. // The receiver gets amount minus fees. String paymentRequest = ""; BigInt? amountSats = BigInt.from(50000); final request = PrepareSendPaymentRequest( paymentRequest: PaymentRequest.input(input: paymentRequest), amount: amountSats, tokenIdentifier: null, conversionOptions: null, feePolicy: FeePolicy.feesIncluded); final response = await sdk.prepareSendPayment(request: request); // The response shows the fee policy used print("Fee policy: ${response.feePolicy}"); print("Amount: ${response.amount}"); // The receiver gets amount - fees (fees are available in response.paymentMethod) ``` ### Python ```python # By default (FeePolicy.FEES_EXCLUDED), fees are added on top of the amount. # Use FeePolicy.FEES_INCLUDED to deduct fees from the amount instead. # The receiver gets amount minus fees. payment_request = "" amount_sats = 50_000 try: request = PrepareSendPaymentRequest( payment_request=PaymentRequest.INPUT(input=payment_request), amount=amount_sats, token_identifier=None, conversion_options=None, fee_policy=FeePolicy.FEES_INCLUDED, ) prepare_response = await sdk.prepare_send_payment(request=request) # The response shows the fee policy used logging.debug(f"Fee policy: {prepare_response.fee_policy}") logging.debug(f"Amount: {prepare_response.amount}") # The receiver gets amount - fees (fees are available in prepare_response.payment_method) except Exception as error: logging.error(error) raise ``` ### Go ```go // By default (FeePolicyFeesExcluded), fees are added on top of the amount. // Use FeePolicyFeesIncluded to deduct fees from the amount instead. // The receiver gets amount minus fees. paymentRequest := "" amountSats := new(big.Int).SetInt64(50_000) feePolicy := breez_sdk_spark.FeePolicyFeesIncluded request := breez_sdk_spark.PrepareSendPaymentRequest{ PaymentRequest: breez_sdk_spark.PaymentRequestInput{Input: paymentRequest}, Amount: &amountSats, TokenIdentifier: nil, ConversionOptions: nil, FeePolicy: &feePolicy, } response, err := sdk.PrepareSendPayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } // The response shows the fee policy used log.Printf("Fee policy: %v", response.FeePolicy) log.Printf("Amount: %v", response.Amount) // The receiver gets amount - fees (fees are available in response.PaymentMethod) ``` When [stable balance](/guide/stable_balance.md) is active, you can send your entire wallet balance — both the token balance and any remaining sats — by combining `FeePolicy::FeesIncluded` with `ConversionType::ToBitcoin` conversion options. See [Sending entire balance](/guide/stable_balance.md#sending-entire-balance) for details. ### Rust ```rust let payment_request = "".to_string(); let token_identifier = "".to_string(); let info = sdk .get_info(GetInfoRequest { ensure_synced: Some(false), }) .await?; let token_balance = info .token_balances .get(&token_identifier) .ok_or_else(|| anyhow::anyhow!("Token balance not found"))?; let conversion_options = Some(ConversionOptions { conversion_type: ConversionType::ToBitcoin { from_token_identifier: token_identifier.clone(), }, max_slippage_bps: None, completion_timeout_secs: None, }); let prepare_response = sdk .prepare_send_payment(PrepareSendPaymentRequest { payment_request: PaymentRequest::Input { input: payment_request, }, amount: Some(token_balance.balance), token_identifier: Some(token_identifier), conversion_options, fee_policy: Some(FeePolicy::FeesIncluded), }) .await?; // The response amount is the estimated total sats available // (converted sats + existing sat balance) info!("Total sats available: {}", prepare_response.amount); if let Some(conversion_estimate) = &prepare_response.conversion_estimate { info!( "Converting {} token units → ~{} sats", conversion_estimate.amount_in, conversion_estimate.amount_out ); info!("Conversion fee: {} token units", conversion_estimate.fee); } ``` ### Swift ```swift let paymentRequest = "" let tokenIdentifier = "" let info = try await sdk.getInfo( request: GetInfoRequest(ensureSynced: false)) guard let tokenBalance = info.tokenBalances[tokenIdentifier] else { throw SdkError.InvalidInput("Token balance not found") } let conversionOptions = ConversionOptions( conversionType: ConversionType.toBitcoin( fromTokenIdentifier: tokenIdentifier ), maxSlippageBps: nil, completionTimeoutSecs: nil ) let prepareResponse = try await sdk.prepareSendPayment( request: PrepareSendPaymentRequest( paymentRequest: .input(input: paymentRequest), amount: tokenBalance.balance, tokenIdentifier: tokenIdentifier, conversionOptions: conversionOptions, feePolicy: .feesIncluded )) // The response amount is the estimated total sats available // (converted sats + existing sat balance) print("Total sats available: \(prepareResponse.amount)") if let conversionEstimate = prepareResponse.conversionEstimate { print( "Converting \(conversionEstimate.amountIn) token units " + "→ ~\(conversionEstimate.amountOut) sats") print("Conversion fee: \(conversionEstimate.fee) token units") } ``` ### Kotlin ```kotlin val paymentRequest = "" val tokenIdentifier = "" try { val info = sdk.getInfo(GetInfoRequest(false)) val tokenBalance = info.tokenBalances[tokenIdentifier] ?: throw Exception("Token balance not found") val conversionOptions = ConversionOptions( conversionType = ConversionType.ToBitcoin( tokenIdentifier ), maxSlippageBps = null, completionTimeoutSecs = null ) val req = PrepareSendPaymentRequest( paymentRequest = PaymentRequest.Input(input = paymentRequest), amount = tokenBalance.balance, tokenIdentifier = tokenIdentifier, conversionOptions = conversionOptions, feePolicy = FeePolicy.FEES_INCLUDED, ) val prepareResponse = sdk.prepareSendPayment(req) // The response amount is the estimated total sats available // (converted sats + existing sat balance) // Log.v("Breez", "Total sats available: ${prepareResponse.amount}") prepareResponse.conversionEstimate?.let { conversionEstimate -> // Log.v("Breez", "Converting ${conversionEstimate.amountIn} token // units → ~${conversionEstimate.amountOut} sats") // Log.v("Breez", "Conversion fee: ${conversionEstimate.fee} token units") } } catch (e: Exception) { // handle error } ``` ### C# ```csharp var paymentRequest = ""; var tokenIdentifier = ""; var info = await sdk.GetInfo(request: new GetInfoRequest(ensureSynced: false)); if (!info.tokenBalances.TryGetValue(tokenIdentifier, out var tokenBalance)) { throw new Exception("Token balance not found"); } var conversionOptions = new ConversionOptions( conversionType: new ConversionType.ToBitcoin( fromTokenIdentifier: tokenIdentifier ), maxSlippageBps: null, completionTimeoutSecs: null ); var request = new PrepareSendPaymentRequest( paymentRequest: new PaymentRequest.Input(input: paymentRequest), amount: tokenBalance.balance, tokenIdentifier: tokenIdentifier, conversionOptions: conversionOptions, feePolicy: FeePolicy.FeesIncluded ); var prepareResponse = await sdk.PrepareSendPayment(request: request); // The response amount is the estimated total sats available // (converted sats + existing sat balance) Console.WriteLine($"Total sats available: {prepareResponse.amount}"); if (prepareResponse.conversionEstimate != null) { Console.WriteLine("Converting " + $"{prepareResponse.conversionEstimate.amountIn} token units " + $"→ ~{prepareResponse.conversionEstimate.amountOut} sats"); Console.WriteLine("Conversion fee: " + $"{prepareResponse.conversionEstimate.fee} token units"); } ``` ### Javascript (Wasm) ```typescript const paymentRequest = '' const tokenIdentifier = '' const info = await sdk.getInfo({ ensureSynced: false }) const tokenBalance = info.tokenBalances.get(tokenIdentifier) if (tokenBalance === undefined) { throw new Error('Token balance not found') } const conversionOptions: ConversionOptions = { conversionType: { type: 'toBitcoin', fromTokenIdentifier: tokenIdentifier } } const feePolicy: FeePolicy = 'feesIncluded' const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: { type: 'input', input: paymentRequest }, amount: tokenBalance.balance, tokenIdentifier, conversionOptions, feePolicy }) // The response amount is the estimated total sats available // (converted sats + existing sat balance) console.log(`Total sats available: ${prepareResponse.amount}`) if (prepareResponse.conversionEstimate !== undefined) { const estimate = prepareResponse.conversionEstimate console.log(`Converting ${estimate.amountIn} token units → ~${estimate.amountOut} sats`) console.log(`Conversion fee: ${estimate.fee} token units`) } ``` ### React Native ```typescript const paymentRequest = '' const tokenIdentifier = '' const info = await sdk.getInfo({ ensureSynced: false }) const tokenBalance = info.tokenBalances.get(tokenIdentifier) if (tokenBalance === undefined) { throw new Error('Token balance not found') } const conversionOptions = { conversionType: new ConversionType.ToBitcoin({ fromTokenIdentifier: tokenIdentifier }), maxSlippageBps: undefined, completionTimeoutSecs: undefined } const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: new PaymentRequest.Input({ input: paymentRequest }), amount: tokenBalance.balance, tokenIdentifier, conversionOptions, feePolicy: FeePolicy.FeesIncluded }) // The response amount is the estimated total sats available // (converted sats + existing sat balance) console.log(`Total sats available: ${prepareResponse.amount}`) if (prepareResponse.conversionEstimate !== undefined) { const estimate = prepareResponse.conversionEstimate console.log(`Converting ${estimate.amountIn} token units → ~${estimate.amountOut} sats`) console.log(`Conversion fee: ${estimate.fee} token units`) } ``` ### Flutter ```dart String paymentRequest = ""; String tokenIdentifier = ""; final info = await sdk.getInfo(request: GetInfoRequest(ensureSynced: false)); final tokenBalance = info.tokenBalances[tokenIdentifier]; if (tokenBalance == null) { throw Exception("Token balance not found"); } final conversionOptions = ConversionOptions( conversionType: ConversionType.toBitcoin( fromTokenIdentifier: tokenIdentifier, ), ); final request = PrepareSendPaymentRequest( paymentRequest: PaymentRequest.input(input: paymentRequest), amount: tokenBalance.balance, tokenIdentifier: tokenIdentifier, conversionOptions: conversionOptions, feePolicy: FeePolicy.feesIncluded); final response = await sdk.prepareSendPayment(request: request); // The response amount is the estimated total sats available // (converted sats + existing sat balance) print("Total sats available: ${response.amount}"); if (response.conversionEstimate != null) { print( "Converting ${response.conversionEstimate!.amountIn} token units " "→ ~${response.conversionEstimate!.amountOut} sats"); print( "Conversion fee: ${response.conversionEstimate!.fee} token units"); } ``` ### Python ```python payment_request = "" token_identifier = "" try: info = await sdk.get_info(request=GetInfoRequest(ensure_synced=False)) token_balance = info.token_balances.get(token_identifier) if token_balance is None: raise ValueError("Token balance not found") conversion_options = ConversionOptions( conversion_type=ConversionType.TO_BITCOIN( from_token_identifier=token_identifier ), ) request = PrepareSendPaymentRequest( payment_request=PaymentRequest.INPUT(input=payment_request), amount=token_balance.balance, token_identifier=token_identifier, conversion_options=conversion_options, fee_policy=FeePolicy.FEES_INCLUDED, ) prepare_response = await sdk.prepare_send_payment(request=request) # The response amount is the estimated total sats available # (converted sats + existing sat balance) logging.debug(f"Total sats available: {prepare_response.amount}") if prepare_response.conversion_estimate is not None: conversion_estimate = prepare_response.conversion_estimate logging.debug( f"Converting {conversion_estimate.amount_in}" f" token units → ~{conversion_estimate.amount_out} sats" ) logging.debug( f"Conversion fee: {conversion_estimate.fee} token units" ) except Exception as error: logging.error(error) raise ``` ### Go ```go paymentRequest := "" tokenIdentifier := "" ensureSynced := false info, err := sdk.GetInfo(breez_sdk_spark.GetInfoRequest{ EnsureSynced: &ensureSynced, }) if err != nil { return nil, err } tokenBalance, ok := info.TokenBalances[tokenIdentifier] if !ok { return nil, errors.New("token balance not found") } conversionOptions := breez_sdk_spark.ConversionOptions{ ConversionType: breez_sdk_spark.ConversionTypeToBitcoin{ FromTokenIdentifier: tokenIdentifier, }, } feePolicy := breez_sdk_spark.FeePolicyFeesIncluded request := breez_sdk_spark.PrepareSendPaymentRequest{ PaymentRequest: breez_sdk_spark.PaymentRequestInput{Input: paymentRequest}, Amount: &tokenBalance.Balance, TokenIdentifier: &tokenIdentifier, ConversionOptions: &conversionOptions, FeePolicy: &feePolicy, } response, err := sdk.PrepareSendPayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } // The response amount is the estimated total sats available // (converted sats + existing sat balance) log.Printf("Total sats available: %v", response.Amount) if response.ConversionEstimate != nil { log.Printf( "Converting %v token units → ~%v sats", response.ConversionEstimate.AmountIn, response.ConversionEstimate.AmountOut, ) log.Printf("Conversion fee: %v token units", response.ConversionEstimate.Fee) } ``` ## Sending Payments API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.send_payment Once the payment has been prepared and the fees are accepted, the payment can be sent by passing: - **Prepare Response** - The response from the [Preparing the Payment](/guide/send_payment.md#preparing-payments) step. - **Options** - Any payment method specific options for the payment (see below). - **Idempotency Key** - An optional UUID that identifies the payment. If set, providing the same idempotency key for multiple requests will ensure that only one payment is made. ### Lightning In the optional send payment options for BOLT11 invoices, you can set: - **Prefer Spark** - Set the preference to use Spark to transfer the payment if the invoice contains a Spark address. By default, using Spark transfers are disabled. - **Completion Timeout** - By default, this function returns immediately. You can override this behavior by specifying a completion timeout in seconds. If the timeout is reached, a pending payment object is returned. If the payment completes within the timeout, the completed payment object is returned. #### Rust ```rust let options = Some(SendPaymentOptions::Bolt11Invoice { prefer_spark: false, completion_timeout_secs: Some(10), }); let optional_idempotency_key = Some("".to_string()); let send_response = sdk .send_payment(SendPaymentRequest { prepare_response, options, idempotency_key: optional_idempotency_key, }) .await?; let payment = send_response.payment; info!("Payment: {payment:?}"); ``` #### Swift ```swift let options = SendPaymentOptions.bolt11Invoice(preferSpark: false, completionTimeoutSecs: 10) let optionalIdempotencyKey = "" let sendResponse = try await sdk.sendPayment( request: SendPaymentRequest( prepareResponse: prepareResponse, options: options, idempotencyKey: optionalIdempotencyKey )) let payment = sendResponse.payment ``` #### Kotlin ```kotlin try { val options = SendPaymentOptions.Bolt11Invoice( preferSpark = false, completionTimeoutSecs = 10u ) val optionalIdempotencyKey = "" val sendResponse = sdk.sendPayment( SendPaymentRequest( prepareResponse, options, optionalIdempotencyKey ) ) val payment = sendResponse.payment } catch (e: Exception) { // handle error } ``` #### C# ```csharp var options = new SendPaymentOptions.Bolt11Invoice( preferSpark: false, completionTimeoutSecs: 10 ); var optionalIdempotencyKey = ""; var request = new SendPaymentRequest( prepareResponse: prepareResponse, options: options, idempotencyKey: optionalIdempotencyKey ); var sendResponse = await sdk.SendPayment(request: request); var payment = sendResponse.payment; ``` #### Javascript (Wasm) ```typescript const options: SendPaymentOptions = { type: 'bolt11Invoice', preferSpark: false, completionTimeoutSecs: 10 } const optionalIdempotencyKey = '' const sendResponse = await sdk.sendPayment({ prepareResponse, options, idempotencyKey: optionalIdempotencyKey }) const payment = sendResponse.payment ``` #### React Native ```typescript const options = new SendPaymentOptions.Bolt11Invoice({ preferSpark: false, completionTimeoutSecs: 10 }) const optionalIdempotencyKey = '' const sendResponse = await sdk.sendPayment({ prepareResponse, options, idempotencyKey: optionalIdempotencyKey }) const payment = sendResponse.payment ``` #### Flutter ```dart final options = SendPaymentOptions.bolt11Invoice( preferSpark: false, completionTimeoutSecs: 10); String? optionalIdempotencyKey = ""; final request = SendPaymentRequest( prepareResponse: prepareResponse, options: options, idempotencyKey: optionalIdempotencyKey); SendPaymentResponse response = await sdk.sendPayment(request: request); Payment payment = response.payment; ``` #### Python ```python try: options = SendPaymentOptions.BOLT11_INVOICE( prefer_spark=False, completion_timeout_secs=10 ) optional_idempotency_key = "" request = SendPaymentRequest( prepare_response=prepare_response, options=options, idempotency_key=optional_idempotency_key, ) send_response = await sdk.send_payment(request=request) payment = send_response.payment except Exception as error: logging.error(error) raise ``` #### Go ```go var completionTimeoutSecs uint32 = 10 var options breez_sdk_spark.SendPaymentOptions = breez_sdk_spark.SendPaymentOptionsBolt11Invoice{ PreferSpark: false, CompletionTimeoutSecs: &completionTimeoutSecs, } optionalIdempotencyKey := "" request := breez_sdk_spark.SendPaymentRequest{ PrepareResponse: prepareResponse, Options: &options, IdempotencyKey: &optionalIdempotencyKey, } response, err := sdk.SendPayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } payment := response.Payment ``` ### Bitcoin In the optional send payment options for Bitcoin addresses, you can set: - **Confirmation Speed** - The priority that the Bitcoin transaction confirms, that also effects the fee paid. By default, it is set to Fast. #### Rust ```rust // Select the confirmation speed for the on-chain transaction let options = Some(SendPaymentOptions::BitcoinAddress { confirmation_speed: OnchainConfirmationSpeed::Medium, }); let optional_idempotency_key = Some("".to_string()); let send_response = sdk .send_payment(SendPaymentRequest { prepare_response, options, idempotency_key: optional_idempotency_key, }) .await?; let payment = send_response.payment; info!("Payment: {payment:?}"); ``` #### Swift ```swift // Select the confirmation speed for the on-chain transaction let options = SendPaymentOptions.bitcoinAddress( confirmationSpeed: OnchainConfirmationSpeed.medium ) let optionalIdempotencyKey = "" let sendResponse = try await sdk.sendPayment( request: SendPaymentRequest( prepareResponse: prepareResponse, options: options, idempotencyKey: optionalIdempotencyKey )) let payment = sendResponse.payment ``` #### Kotlin ```kotlin try { // Select the confirmation speed for the on-chain transaction val options = SendPaymentOptions.BitcoinAddress( confirmationSpeed = OnchainConfirmationSpeed.MEDIUM ) val optionalIdempotencyKey = "" val sendResponse = sdk.sendPayment( SendPaymentRequest( prepareResponse, options, optionalIdempotencyKey ) ) val payment = sendResponse.payment } catch (e: Exception) { // handle error } ``` #### C# ```csharp // Select the confirmation speed for the on-chain transaction var options = new SendPaymentOptions.BitcoinAddress( confirmationSpeed: OnchainConfirmationSpeed.Medium ); var optionalIdempotencyKey = ""; var request = new SendPaymentRequest( prepareResponse: prepareResponse, options: options, idempotencyKey: optionalIdempotencyKey ); var sendResponse = await sdk.SendPayment(request: request); var payment = sendResponse.payment; ``` #### Javascript (Wasm) ```typescript // Select the confirmation speed for the on-chain transaction const options: SendPaymentOptions = { type: 'bitcoinAddress', confirmationSpeed: 'medium' } const optionalIdempotencyKey = '' const sendResponse = await sdk.sendPayment({ prepareResponse, options, idempotencyKey: optionalIdempotencyKey }) const payment = sendResponse.payment ``` #### React Native ```typescript // Select the confirmation speed for the on-chain transaction const options = new SendPaymentOptions.BitcoinAddress({ confirmationSpeed: OnchainConfirmationSpeed.Medium }) const optionalIdempotencyKey = '' const sendResponse = await sdk.sendPayment({ prepareResponse, options, idempotencyKey: optionalIdempotencyKey }) const payment = sendResponse.payment ``` #### Flutter ```dart // Select the confirmation speed for the on-chain transaction final options = SendPaymentOptions.bitcoinAddress( confirmationSpeed: OnchainConfirmationSpeed.medium); String? optionalIdempotencyKey = ""; final request = SendPaymentRequest( prepareResponse: prepareResponse, options: options, idempotencyKey: optionalIdempotencyKey); SendPaymentResponse response = await sdk.sendPayment(request: request); Payment payment = response.payment; ``` #### Python ```python try: # Select the confirmation speed for the on-chain transaction options = SendPaymentOptions.BITCOIN_ADDRESS( confirmation_speed=OnchainConfirmationSpeed.MEDIUM ) optional_idempotency_key = "" request = SendPaymentRequest( prepare_response=prepare_response, options=options, idempotency_key=optional_idempotency_key, ) send_response = await sdk.send_payment(request=request) payment = send_response.payment except Exception as error: logging.error(error) raise ``` #### Go ```go // Select the confirmation speed for the on-chain transaction var options breez_sdk_spark.SendPaymentOptions = breez_sdk_spark.SendPaymentOptionsBitcoinAddress{ ConfirmationSpeed: breez_sdk_spark.OnchainConfirmationSpeedMedium, } optionalIdempotencyKey := "" request := breez_sdk_spark.SendPaymentRequest{ PrepareResponse: prepareResponse, Options: &options, IdempotencyKey: &optionalIdempotencyKey, } response, err := sdk.SendPayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } payment := response.Payment ``` ### Spark In the optional send payment options for Spark addresses, you can set: - **HTLC Options** - Enables Spark HTLC payments, which are an advanced feature that allows for conditional payments. See the [Spark HTLC Payments](/guide/htlcs.md) page for more details and example usage. #### Rust ```rust let optional_idempotency_key = Some("".to_string()); let send_response = sdk .send_payment(SendPaymentRequest { prepare_response, options: None, idempotency_key: optional_idempotency_key, }) .await?; let payment = send_response.payment; info!("Payment: {payment:?}"); ``` #### Swift ```swift let optionalIdempotencyKey = "" let sendResponse = try await sdk.sendPayment( request: SendPaymentRequest( prepareResponse: prepareResponse, idempotencyKey: optionalIdempotencyKey )) let payment = sendResponse.payment ``` #### Kotlin ```kotlin try { val optionalIdempotencyKey = "" val sendResponse = sdk.sendPayment( SendPaymentRequest( prepareResponse, idempotencyKey = optionalIdempotencyKey ) ) val payment = sendResponse.payment } catch (e: Exception) { // handle error } ``` #### C# ```csharp var optionalIdempotencyKey = ""; var request = new SendPaymentRequest( prepareResponse: prepareResponse, idempotencyKey: optionalIdempotencyKey ); var sendResponse = await sdk.SendPayment(request: request); var payment = sendResponse.payment; ``` #### Javascript (Wasm) ```typescript const optionalIdempotencyKey = '' const sendResponse = await sdk.sendPayment({ prepareResponse, idempotencyKey: optionalIdempotencyKey }) const payment = sendResponse.payment ``` #### React Native ```typescript const optionalIdempotencyKey = '' const sendResponse = await sdk.sendPayment({ prepareResponse, options: undefined, idempotencyKey: optionalIdempotencyKey }) const payment = sendResponse.payment ``` #### Flutter ```dart String? optionalIdempotencyKey = ""; final request = SendPaymentRequest( prepareResponse: prepareResponse, idempotencyKey: optionalIdempotencyKey); SendPaymentResponse response = await sdk.sendPayment(request: request); Payment payment = response.payment; ``` #### Python ```python try: optional_idempotency_key = "" request = SendPaymentRequest( prepare_response=prepare_response, idempotency_key=optional_idempotency_key ) send_response = await sdk.send_payment(request=request) payment = send_response.payment except Exception as error: logging.error(error) raise ``` #### Go ```go optionalIdempotencyKey := "" request := breez_sdk_spark.SendPaymentRequest{ PrepareResponse: prepareResponse, IdempotencyKey: &optionalIdempotencyKey, } response, err := sdk.SendPayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } payment := response.Payment ``` ### USDC/USDT Send USDC/USDT has no additional send payment options. #### Rust ```rust // Only valid for sends with no token leg (see Retry safety). let optional_idempotency_key = Some("".to_string()); let send_response = sdk .send_payment(SendPaymentRequest { prepare_response, options: None, idempotency_key: optional_idempotency_key, }) .await?; let payment = send_response.payment; info!("Payment: {payment:?}"); ``` #### Swift ```swift // Only valid for sends with no token leg (see Retry safety). let optionalIdempotencyKey = "" let sendResponse = try await sdk.sendPayment( request: SendPaymentRequest( prepareResponse: prepareResponse, options: nil, idempotencyKey: optionalIdempotencyKey )) let payment = sendResponse.payment print(payment) ``` #### Kotlin ```kotlin // Only valid for sends with no token leg (see Retry safety). val optionalIdempotencyKey = "" try { val req = SendPaymentRequest( prepareResponse = prepareResponse, options = null, idempotencyKey = optionalIdempotencyKey, ) val sendResponse = sdk.sendPayment(req) val payment = sendResponse.payment // Log.v("Breez", "Payment: $payment") } catch (e: Exception) { // handle error } ``` #### C# ```csharp // Only valid for sends with no token leg (see Retry safety). var optionalIdempotencyKey = ""; var request = new SendPaymentRequest( prepareResponse: prepareResponse, options: null, idempotencyKey: optionalIdempotencyKey ); var sendResponse = await sdk.SendPayment(request: request); Console.WriteLine($"Payment: {sendResponse.payment}"); ``` #### Javascript (Wasm) ```typescript // Only valid for sends with no token leg (see Retry safety). const optionalIdempotencyKey = '' const sendResponse = await sdk.sendPayment({ prepareResponse, options: undefined, idempotencyKey: optionalIdempotencyKey }) const payment = sendResponse.payment console.debug('Payment:', payment) ``` #### React Native ```typescript // Only valid for sends with no token leg (see Retry safety). const optionalIdempotencyKey = '' const sendResponse = await sdk.sendPayment({ prepareResponse, options: undefined, idempotencyKey: optionalIdempotencyKey }) console.debug('Payment:', sendResponse.payment) ``` #### Flutter ```dart // Only valid for sends with no token leg (see Retry safety). String? optionalIdempotencyKey = ""; final request = SendPaymentRequest( prepareResponse: prepareResponse, options: null, idempotencyKey: optionalIdempotencyKey, ); final response = await sdk.sendPayment(request: request); print("Payment: ${response.payment}"); ``` #### Python ```python # Only valid for sends with no token leg (see Retry safety). optional_idempotency_key = "" try: request = SendPaymentRequest( prepare_response=prepare_response, options=None, idempotency_key=optional_idempotency_key, ) send_response = await sdk.send_payment(request=request) payment = send_response.payment logging.debug(f"Payment: {payment}") except Exception as error: logging.error(error) raise ``` #### Go ```go // Only valid for sends with no token leg (see Retry safety). optionalIdempotencyKey := "" request := breez_sdk_spark.SendPaymentRequest{ PrepareResponse: prepareResponse, Options: nil, IdempotencyKey: &optionalIdempotencyKey, } response, err := sdk.SendPayment(request) if err != nil { return nil, err } log.Printf("Payment: %v", response.Payment) ``` ## Event Flows Once a send payment is initiated, you can follow and react to the different payment events using the guide below for each payment method. See [listening to events](/guide/events.html) for how to subscribe to events. The `SdkEvent::Synced` event is also emitted as the SDK syncs in the background. See [fetching the balance](/guide/get_info.md) for the recommended pattern for refreshing the balance and payments list. #### Lightning | Event | Description | UX Suggestion | | -------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------ | | **PaymentPending** | The Spark transfer has been started. Awaiting Lightning payment completion. | Show payment as pending. | | **PaymentSucceeded** | The Lightning invoice has been paid either over Lightning or via a Spark transfer | Show the payment as complete and call `get_info` to read the updated balance. The SDK refreshes the cached balance before emitting this event. See [fetching the balance](/guide/get_info.md). | | **PaymentFailed** | The attempt to pay the Lightning invoice failed. | | #### Bitcoin | Event | Description | UX Suggestion | | -------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------ | | **PaymentPending** | The Spark transfer has been started. Awaiting on-chain withdrawal completion. | Show payment as pending. | | **PaymentSucceeded** | The payment amount was successfully withdrawn on-chain. | Show the payment as complete and call `get_info` to read the updated balance. The SDK refreshes the cached balance before emitting this event. See [fetching the balance](/guide/get_info.md). | #### Spark | Event | Description | UX Suggestion | | -------------------- | ------------------------------- | ------------------------------------------------ | | **PaymentSucceeded** | The Spark transfer is complete. | Show the payment as complete and call `get_info` to read the updated balance. The SDK refreshes the cached balance before emitting this event. See [fetching the balance](/guide/get_info.md). | #### USDC/USDT | Event | Description | UX Suggestion | | -------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | | **PaymentPending** | The deposit transfer has been submitted to the provider. The cross-chain leg is awaiting settlement. | Show payment as pending; the bridge leg may take several minutes depending on the provider and destination chain. | | **PaymentSucceeded** | The provider reports the cross-chain order terminal. The amount actually delivered to the recipient is carried on the conversion info. | Show the payment as complete and call `get_info` to read the updated balance. The SDK refreshes the cached balance before emitting this event. See [fetching the balance](/guide/get_info.md). | --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Listing payments](/guide/list_payments.md)** # Listing payments API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.list_payments To view your payment history, you can list all the payments that have been sent and received. ## Rust ```rust let response = sdk.list_payments(ListPaymentsRequest::default()).await?; let payments = response.payments; ``` ## Swift ```swift let response = try await sdk.listPayments( request: ListPaymentsRequest()) let payments = response.payments ``` ## Kotlin ```kotlin try { val response = sdk.listPayments(ListPaymentsRequest()) val payments = response.payments } catch (e: Exception) { // handle error } ``` ## C# ```csharp var response = await sdk.ListPayments(request: new ListPaymentsRequest()); var payments = response.payments; ``` ## Javascript (Wasm) ```typescript const response = await sdk.listPayments({}) const payments = response.payments ``` ## React Native ```typescript const response = await sdk.listPayments({ typeFilter: undefined, statusFilter: undefined, assetFilter: undefined, paymentDetailsFilter: undefined, fromTimestamp: undefined, toTimestamp: undefined, offset: undefined, limit: undefined, sortAscending: undefined }) const payments = response.payments ``` ## Flutter ```dart ListPaymentsRequest request = ListPaymentsRequest(); ListPaymentsResponse response = await sdk.listPayments(request: request); List payments = response.payments; ``` ## Python ```python response = await sdk.list_payments(request=ListPaymentsRequest()) payments = response.payments ``` ## Go ```go response, err := sdk.ListPayments(breez_sdk_spark.ListPaymentsRequest{}) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } payments := response.Payments ``` ## Filtering Payments When listing payments you can also filter and page the results. ### Rust ```rust // Filter by asset (Bitcoin or Token) let asset_filter = AssetFilter::Token { token_identifier: Some("token_identifier_here".to_string()), }; // To filter by Bitcoin instead: // let asset_filter = AssetFilter::Bitcoin; let response = sdk .list_payments(ListPaymentsRequest { // Filter by payment type type_filter: Some(vec![PaymentType::Send, PaymentType::Receive]), // Filter by status status_filter: Some(vec![PaymentStatus::Completed]), asset_filter: Some(asset_filter), // Time range filters from_timestamp: Some(1704067200), // Unix timestamp to_timestamp: Some(1735689600), // Unix timestamp // Pagination offset: Some(0), limit: Some(50), // Sort order (true = oldest first, false = newest first) sort_ascending: Some(false), payment_details_filter: None, }) .await?; let payments = response.payments; ``` ### Swift ```swift // Filter by asset (Bitcoin or Token) let assetFilter = AssetFilter.token(tokenIdentifier: "token_identifier_here") // To filter by Bitcoin instead: // let assetFilter = AssetFilter.bitcoin let response = try await sdk.listPayments( request: ListPaymentsRequest( // Filter by payment type typeFilter: [PaymentType.send, PaymentType.receive], // Filter by status statusFilter: [PaymentStatus.completed], assetFilter: assetFilter, // Time range filters fromTimestamp: 1_704_067_200, // Unix timestamp toTimestamp: 1_735_689_600, // Unix timestamp // Pagination offset: 0, limit: 50, // Sort order (true = oldest first, false = newest first) sortAscending: false )) let payments = response.payments ``` ### Kotlin ```kotlin try { // Filter by asset (Bitcoin or Token) val assetFilter = AssetFilter.Token(tokenIdentifier = "token_identifier_here") // To filter by Bitcoin instead: // val assetFilter = AssetFilter.Bitcoin val response = sdk.listPayments( ListPaymentsRequest( // Filter by payment type typeFilter = listOf(PaymentType.SEND, PaymentType.RECEIVE), // Filter by status statusFilter = listOf(PaymentStatus.COMPLETED), assetFilter = assetFilter, // Time range filters fromTimestamp = 1704067200u, // Unix timestamp toTimestamp = 1735689600u, // Unix timestamp // Pagination offset = 0u, limit = 50u, // Sort order (true = oldest first, false = newest first) sortAscending = false )) val payments = response.payments } catch (e: Exception) { // handle error } ``` ### C# ```csharp // Filter by asset (Bitcoin or Token) var assetFilter = new AssetFilter.Token(tokenIdentifier: "token_identifier_here"); // To filter by Bitcoin instead: // var assetFilter = new AssetFilter.Bitcoin(); var request = new ListPaymentsRequest( // Filter by payment type typeFilter: new PaymentType[] { PaymentType.Send, PaymentType.Receive }, // Filter by status statusFilter: new PaymentStatus[] { PaymentStatus.Completed }, assetFilter: assetFilter, // Time range filters fromTimestamp: 1704067200, // Unix timestamp toTimestamp: 1735689600, // Unix timestamp // Pagination offset: 0, limit: 50, // Sort order (true = oldest first, false = newest first) sortAscending: false ); var response = await sdk.ListPayments(request: request); var payments = response.payments; ``` ### Javascript (Wasm) ```typescript // Filter by asset (Bitcoin or Token) const assetFilter: AssetFilter = { type: 'token', tokenIdentifier: 'token_identifier_here' } // To filter by Bitcoin instead: // const assetFilter: AssetFilter = { type: 'bitcoin' } const response = await sdk.listPayments({ // Filter by payment type typeFilter: ['send', 'receive'], // Filter by status statusFilter: ['completed'], assetFilter, // Time range filters fromTimestamp: 1704067200, // Unix timestamp toTimestamp: 1735689600, // Unix timestamp // Pagination offset: 0, limit: 50, // Sort order (true = oldest first, false = newest first) sortAscending: false }) const payments = response.payments ``` ### React Native ```typescript // Filter by asset (Bitcoin or Token) const assetFilter = new AssetFilter.Token({ tokenIdentifier: 'token_identifier_here' }) // To filter by Bitcoin instead: // const assetFilter = new AssetFilter.Bitcoin() const response = await sdk.listPayments({ // Filter by payment type typeFilter: [PaymentType.Send, PaymentType.Receive], // Filter by status statusFilter: [PaymentStatus.Completed], assetFilter, paymentDetailsFilter: undefined, // Time range filters fromTimestamp: 1704067200n, // Unix timestamp toTimestamp: 1735689600n, // Unix timestamp // Pagination offset: 0, limit: 50, // Sort order (true = oldest first, false = newest first) sortAscending: false }) const payments = response.payments ``` ### Flutter ```dart // Filter by asset (Bitcoin or Token) AssetFilter assetFilter = AssetFilter.token(tokenIdentifier: "token_identifier_here"); // To filter by Bitcoin instead: // AssetFilter assetFilter = AssetFilter.bitcoin(); ListPaymentsRequest request = ListPaymentsRequest( // Filter by payment type typeFilter: [PaymentType.send, PaymentType.receive], // Filter by status statusFilter: [PaymentStatus.completed], assetFilter: assetFilter, // Time range filters fromTimestamp: BigInt.from(1704067200), // Unix timestamp toTimestamp: BigInt.from(1735689600), // Unix timestamp // Pagination offset: 0, limit: 50, // Sort order (true = oldest first, false = newest first) sortAscending: false, ); ListPaymentsResponse response = await sdk.listPayments(request: request); List payments = response.payments; ``` ### Python ```python # Filter by asset (Bitcoin or Token) asset_filter = AssetFilter.TOKEN(token_identifier="token_identifier_here") # To filter by Bitcoin instead: # asset_filter = AssetFilter.BITCOIN request = ListPaymentsRequest( # Filter by payment type type_filter=[PaymentType.SEND, PaymentType.RECEIVE], # Filter by status status_filter=[PaymentStatus.COMPLETED], asset_filter=asset_filter, # Time range filters from_timestamp=1704067200, # Unix timestamp to_timestamp=1735689600, # Unix timestamp # Pagination offset=0, limit=50, # Sort order (true = oldest first, false = newest first) sort_ascending=False ) response = await sdk.list_payments(request=request) payments = response.payments ``` ### Go ```go // Filter by asset (Bitcoin or Token) tokenIdentifier := "token_identifier_here" var assetFilter breez_sdk_spark.AssetFilter = breez_sdk_spark.AssetFilterToken{ TokenIdentifier: &tokenIdentifier, } // To filter by Bitcoin instead: // var assetFilter breez_sdk_spark.AssetFilter = breez_sdk_spark.AssetFilterBitcoin // Filter options typeFilter := []breez_sdk_spark.PaymentType{ breez_sdk_spark.PaymentTypeSend, breez_sdk_spark.PaymentTypeReceive, } statusFilter := []breez_sdk_spark.PaymentStatus{ breez_sdk_spark.PaymentStatusCompleted, } fromTimestamp := uint64(1704067200) // Unix timestamp toTimestamp := uint64(1735689600) // Unix timestamp offset := uint32(0) limit := uint32(50) sortAscending := false request := breez_sdk_spark.ListPaymentsRequest{ TypeFilter: &typeFilter, // Filter by payment type StatusFilter: &statusFilter, // Filter by status AssetFilter: &assetFilter, // Filter by asset (Bitcoin or Token) FromTimestamp: &fromTimestamp, // Time range filters ToTimestamp: &toTimestamp, // Time range filters Offset: &offset, // Pagination Limit: &limit, // Pagination SortAscending: &sortAscending, // Sort order (true = oldest first, false = newest first) } response, err := sdk.ListPayments(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } payments := response.Payments ``` ## Get Payment API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.get_payment You can also retrieve a single payment using the payment id: ### Rust ```rust let payment_id = "".to_string(); let response = sdk.get_payment(GetPaymentRequest { payment_id }).await?; let payment = response.payment; ``` ### Swift ```swift let paymentId = "" let response = try await sdk.getPayment( request: GetPaymentRequest(paymentId: paymentId) ) let payment = response.payment ``` ### Kotlin ```kotlin try { val paymentId = ""; val response = sdk.getPayment(GetPaymentRequest(paymentId)) val payment = response.payment } catch (e: Exception) { // handle error } ``` ### C# ```csharp var paymentId = ""; var response = await sdk.GetPayment( request: new GetPaymentRequest(paymentId: paymentId) ); var payment = response.payment; ``` ### Javascript (Wasm) ```typescript const paymentId = '' const response = await sdk.getPayment({ paymentId }) const payment = response.payment ``` ### React Native ```typescript const paymentId = '' const response = await sdk.getPayment({ paymentId }) const payment = response.payment ``` ### Flutter ```dart String paymentId = ""; GetPaymentRequest request = GetPaymentRequest(paymentId: paymentId); GetPaymentResponse response = await sdk.getPayment(request: request); Payment payment = response.payment; ``` ### Python ```python payment_id = "" response = await sdk.get_payment( request=GetPaymentRequest(payment_id=payment_id) ) payment = response.payment ``` ### Go ```go paymentId := "" request := breez_sdk_spark.GetPaymentRequest{ PaymentId: paymentId, } response, err := sdk.GetPayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } payment := response.Payment ``` **→ [Claiming on-chain deposits](/guide/onchain_claims.md)** # Claiming on-chain deposits On-chain deposits go through three stages. Once detected, the deposit is visible in the SDK and each deposit includes a `is_mature` field. After **3 on-chain confirmations** the deposit has sufficient confirmations (`is_mature` is true) and the SDK [automatically attempts](#setting-a-max-fee-for-automatic-claims) to claim it. The SDK also claims automatically [before maturity](#claiming-before-maturity) when the configured ceiling covers the provider's spread, so a deposit can be credited sooner than 3 confirmations. If the maximum deposit claim fee is too low for either, the deposit won't be automatically claimed and should be [manually claimed](#manually-claiming-deposits). ## Setting a max fee for automatic claims The [maximum deposit claim fee](/guide/config.md#max-deposit-claim-fee) setting in the SDK configuration defines the maximum fee the SDK uses when automatically claiming an on-chain deposit. The SDK's default fee limit is set to 1 sats/vbyte, which is low and requires manual claiming when fees exceed this threshold. You can set a higher fee, either in sats/vbyte, in absolute sats, or to the fastest recommended fee at the time of claim, with a leeway in sats/vbyte. This ceiling is not only an on-chain fee tolerance. It also caps what the provider may take to credit a deposit [before it matures](#claiming-before-maturity), so the value you choose decides both how much on-chain fee the SDK will pay and whether deposits are claimed early at all. To increase the likelihood of automatically claiming deposits, you may set the maximum fee to the fastest recommended rate at the time of claim, which can result in higher fees. ### Rust ```rust // Create the default config let mut config = default_config(Network::Mainnet); config.api_key = Some("".to_string()); // Set the maximum fee to the fastest network recommended fee at the time of claim // with a leeway of 1 sats/vbyte config.max_deposit_claim_fee = Some(MaxFee::NetworkRecommended { leeway_sat_per_vbyte: 1, }); ``` ### Swift ```swift // Create the default config var config = defaultConfig(network: Network.mainnet) config.apiKey = "" // Set the maximum fee to the fastest network recommended fee at the time of claim // with a leeway of 1 sats/vbyte config.maxDepositClaimFee = MaxFee.networkRecommended(leewaySatPerVbyte: 1) ``` ### Kotlin ```kotlin // Create the default config val config = defaultConfig(Network.MAINNET) config.apiKey = "" // Set the maximum fee to the fastest network recommended fee at the time of claim // with a leeway of 1 sats/vbyte config.maxDepositClaimFee = MaxFee.NetworkRecommended(leewaySatPerVbyte = 1u) ``` ### C# ```csharp // Create the default config var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { apiKey = "" }; // Set the maximum fee to the fastest network recommended fee at the time of claim // with a leeway of 1 sats/vbyte config = config with { maxDepositClaimFee = new MaxFee.NetworkRecommended(leewaySatPerVbyte: 1) }; ``` ### Javascript (Wasm) ```typescript // Create the default config const config = defaultConfig('mainnet') config.apiKey = '' // Set the maximum fee to the fastest network recommended fee at the time of claim // with a leeway of 1 sats/vbyte config.maxDepositClaimFee = { type: 'networkRecommended', leewaySatPerVbyte: 1 } ``` ### React Native ```typescript // Create the default config const config = defaultConfig(Network.Mainnet) config.apiKey = '' // Set the maximum fee to the fastest network recommended fee at the time of claim // with a leeway of 1 sats/vbyte config.maxDepositClaimFee = new MaxFee.NetworkRecommended({ leewaySatPerVbyte: BigInt(1) }) ``` ### Flutter ```dart // Create the default config var config = defaultConfig(network: Network.mainnet); config = config.copyWith(apiKey: ""); // Set the maximum fee to the fastest network recommended fee at the time of claim // with a leeway of 1 sats/vbyte config = config.copyWith( maxDepositClaimFee: MaxFee.networkRecommended(leewaySatPerVbyte: BigInt.from(1))); ``` ### Python ```python # Create the default config config = default_config(network=Network.MAINNET) config.api_key = "" # Set the maximum fee to the fastest network recommended fee at the time of claim # with a leeway of 1 sats/vbyte config.max_deposit_claim_fee = MaxFee.NETWORK_RECOMMENDED(leeway_sat_per_vbyte=1) ``` ### Go ```go // Create the default config config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) apiKey := "" config.ApiKey = &apiKey // Set the maximum fee to the fastest network recommended fee at the time of claim // with a leeway of 1 sats/vbyte networkRecommendedInterface := breez_sdk_spark.MaxFee( breez_sdk_spark.MaxFeeNetworkRecommended{LeewaySatPerVbyte: 1}, ) config.MaxDepositClaimFee = &networkRecommendedInterface ``` However, even when setting a high fee, the SDK might still fail to automatically claim deposits. In these cases, it's recommended to manually claim them by letting the end user accept the required fees. When [manual intervention](#manually-claiming-deposits) is required, the SDK emits an `SdkEvent::UnclaimedDeposits` event containing information about the deposit. See [Listening to events](/guide/events.md) for how to subscribe to events. ## Claiming before maturity A deposit does not have to wait for maturity. The Spark Service Provider will front the credited amount earlier and take a spread for carrying the risk, and the SDK claims this way automatically whenever the spread fits within the [maximum deposit claim fee](/guide/config.md#max-deposit-claim-fee). The default of 1 sat/vbyte works out to about 99 sats, below any spread the provider charges, so deposits are claimed at maturity until the ceiling is raised enough to cover one. The same applies to `claim_deposit`, which claims a not-yet-mature deposit early when its own `max_fee` allows. The spread is largely the on-chain cost of the provider's claim plus a percentage of the deposit, so it grows with the deposit. ## Manually claiming deposits When a deposit cannot be automatically claimed due to the configured maximum fee being too low, you can manually claim it by specifying a higher fee limit. The recommended approach is to display a user interface showing the required fee amount and request user approval before proceeding with manual claiming. Claiming a deposit the SDK is already claiming, whether from a background attempt or another call, returns `SdkError::DepositClaimInProgress`. The claim already running may still succeed, so treat this as transient rather than as a failure to show the user. ### Rust ```rust if let Some(DepositClaimError::MaxDepositClaimFeeExceeded { required_fee_sats, .. }) = &deposit.claim_error { // Show UI to user with the required fee and get approval let user_approved = true; // Replace with actual user approval logic if user_approved { let request = ClaimDepositRequest { txid: deposit.txid.clone(), vout: deposit.vout, max_fee: Some(MaxFee::Fixed { amount: *required_fee_sats, }), }; sdk.claim_deposit(request).await?; } } ``` ### Swift ```swift if case .maxDepositClaimFeeExceeded(_, _, _, let requiredFeeSats, _) = deposit.claimError { // Show UI to user with the required fee and get approval let userApproved = true // Replace with actual user approval logic if userApproved { let claimRequest = ClaimDepositRequest( txid: deposit.txid, vout: deposit.vout, maxFee: MaxFee.fixed(amount: requiredFeeSats) ) try await sdk.claimDeposit(request: claimRequest) } } ``` ### Kotlin ```kotlin try { val claimError = deposit.claimError if (claimError is DepositClaimError.MaxDepositClaimFeeExceeded) { val requiredFee = claimError.requiredFeeSats // Show UI to user with the required fee and get approval val userApproved = true // Replace with actual user approval logic if (userApproved) { val claimRequest = ClaimDepositRequest( txid = deposit.txid, vout = deposit.vout, maxFee = MaxFee.Fixed(requiredFee) ) sdk.claimDeposit(claimRequest) } } } catch (e: Exception) { // handle error } ``` ### C# ```csharp if (deposit.claimError is DepositClaimError.MaxDepositClaimFeeExceeded exceeded) { var requiredFee = exceeded.requiredFeeSats; // Show UI to user with the required fee and get approval var userApproved = true; // Replace with actual user approval logic if (userApproved) { var claimRequest = new ClaimDepositRequest( txid: deposit.txid, vout: deposit.vout, maxFee: new MaxFee.Fixed(amount: requiredFee) ); await sdk.ClaimDeposit(request: claimRequest); } } ``` ### Javascript (Wasm) ```typescript if (deposit.claimError?.type === 'maxDepositClaimFeeExceeded') { const requiredFee = deposit.claimError.requiredFeeSats // Show UI to user with the required fee and get approval const userApproved = true // Replace with actual user approval logic if (userApproved) { const claimRequest: ClaimDepositRequest = { txid: deposit.txid, vout: deposit.vout, maxFee: { type: 'fixed', amount: requiredFee } } await sdk.claimDeposit(claimRequest) } } ``` ### React Native ```typescript if (deposit.claimError?.tag === DepositClaimError_Tags.MaxDepositClaimFeeExceeded) { const requiredFee = deposit.claimError.inner.requiredFeeSats // Show UI to user with the required fee and get approval const userApproved = true // Replace with actual user approval logic if (userApproved) { const claimRequest: ClaimDepositRequest = { txid: deposit.txid, vout: deposit.vout, maxFee: new MaxFee.Fixed({ amount: requiredFee }) } await sdk.claimDeposit(claimRequest) } } ``` ### Flutter ```dart final claimError = deposit.claimError; if (claimError is DepositClaimError_MaxDepositClaimFeeExceeded) { final requiredFee = claimError.requiredFeeSats; // Show UI to user with the required fee and get approval bool userApproved = true; // Replace with actual user approval logic if (userApproved) { final claimRequest = ClaimDepositRequest( txid: deposit.txid, vout: deposit.vout, maxFee: MaxFee.fixed(amount: requiredFee), ); await sdk.claimDeposit(request: claimRequest); } } ``` ### Python ```python try: if isinstance( deposit.claim_error, DepositClaimError.MAX_DEPOSIT_CLAIM_FEE_EXCEEDED ): required_fee = deposit.claim_error.required_fee_sats # Show UI to user with the required fee and get approval user_approved = True # Replace with actual user approval logic if user_approved: claim_request = ClaimDepositRequest( txid=deposit.txid, vout=deposit.vout, max_fee=Fee.FIXED(amount=required_fee), ) await sdk.claim_deposit(request=claim_request) except Exception as error: logging.error(error) raise ``` ### Go ```go if claimErr := *deposit.ClaimError; claimErr != nil { if exceeded, ok := claimErr.(breez_sdk_spark.DepositClaimErrorMaxDepositClaimFeeExceeded); ok { requiredFee := exceeded.RequiredFeeSats // Show UI to user with the required fee and get approval userApproved := true // Replace with actual user approval logic if userApproved { maxFee := breez_sdk_spark.MaxFee(breez_sdk_spark.MaxFeeFixed{Amount: requiredFee}) claimRequest := breez_sdk_spark.ClaimDepositRequest{ Txid: deposit.Txid, Vout: deposit.Vout, MaxFee: &maxFee, } _, err := sdk.ClaimDeposit(claimRequest) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } } } } ``` ### Showing the choice to the user `fetch_claim_deposit_quote` prices both ways of claiming a deposit, so an app can offer the choice rather than deciding for the user. It returns the deposit's current `confirmations` alongside a quote for claiming early and one for claiming at maturity, each carrying the fee and the `confirmations_required`, which is the depth it becomes claimable at rather than a count of blocks still to wait. Subtract the deposit's current confirmations for that: an early claim claimable at 1 confirmation, on a deposit with 0, is available a block from now. The early quote is absent when the provider will not front this particular deposit, and when claiming early would not actually be earlier: once a deposit has matured, or when the provider would only credit at maturity's own depth, waiting is both cheaper and no slower, so there is no choice left to offer. The early quote is priced whether or not the configured [maximum deposit claim fee](/guide/config.md#max-deposit-claim-fee) would allow it, since that ceiling is usually far below a spread and the point of the quote is to let the user decide. Acting on it therefore means passing a `max_fee` to `claim_deposit` of at least the quoted `fee_sats`; with a lower one the call returns `SdkError::MaxDepositClaimFeeExceeded` and the deposit waits for maturity instead. The quote for maturity is always present, but may be flagged `is_estimate` when the provider will not quote a deposit this early, in which case the fee is derived from current on-chain fees and the final one may differ. A claim made before maturity settles asynchronously, so `claim_deposit` returns no payment. Watch for it via `list_payments` or the [payment events](/guide/events.md). #### Rust ```rust let quote = sdk .fetch_claim_deposit_quote(FetchClaimDepositQuoteRequest { txid: deposit.txid.clone(), vout: deposit.vout, }) .await?; // Claiming once the deposit matures, and how many blocks that is away. let blocks_to_wait = quote .mature .confirmations_required .saturating_sub(quote.confirmations); info!( "Wait {} blocks and pay {} sats", blocks_to_wait, quote.mature.fee_sats ); // Claiming earlier, when the provider offers it. if let Some(instant) = "e.instant { let blocks_to_wait = instant .confirmations_required .saturating_sub(quote.confirmations); info!( "Or wait {} blocks and pay {} sats", blocks_to_wait, instant.fee_sats ); } ``` #### Swift ```swift let request = FetchClaimDepositQuoteRequest(txid: deposit.txid, vout: deposit.vout) let quote = try await sdk.fetchClaimDepositQuote(request: request) // Claiming once the deposit matures, and how many blocks that is away. var blocksToWait: UInt32 = 0 if quote.mature.confirmationsRequired > quote.confirmations { blocksToWait = quote.mature.confirmationsRequired - quote.confirmations } print("Wait \(blocksToWait) blocks and pay \(quote.mature.feeSats) sats") // Claiming earlier, when the provider offers it. if let instant = quote.instant { var instantBlocks: UInt32 = 0 if instant.confirmationsRequired > quote.confirmations { instantBlocks = instant.confirmationsRequired - quote.confirmations } print("Or wait \(instantBlocks) blocks and pay \(instant.feeSats) sats") } ``` #### Kotlin ```kotlin try { val quote = sdk.fetchClaimDepositQuote( FetchClaimDepositQuoteRequest( txid = deposit.txid, vout = deposit.vout ) ) // UInt subtraction wraps, so clamp the wait at zero. fun blocksToWait(required: UInt) = if (required > quote.confirmations) required - quote.confirmations else 0u // Claiming once the deposit matures, and how many blocks that is away. val matureWait = blocksToWait(quote.mature.confirmationsRequired) // Log.v("Breez", "Wait $matureWait blocks and pay ${quote.mature.feeSats} sats") // Claiming earlier, when the provider offers it. quote.instant?.let { instant -> val instantWait = blocksToWait(instant.confirmationsRequired) // Log.v("Breez", "Or wait $instantWait blocks and pay ${instant.feeSats} sats") } } catch (e: Exception) { // handle error } ``` #### C# ```csharp var request = new FetchClaimDepositQuoteRequest( txid: deposit.txid, vout: deposit.vout ); var quote = await sdk.FetchClaimDepositQuote(request: request); // Claiming once the deposit matures, and how many blocks that is away. var blocksToWait = quote.mature.confirmationsRequired > quote.confirmations ? quote.mature.confirmationsRequired - quote.confirmations : 0U; Console.WriteLine($"Wait {blocksToWait} blocks and pay {quote.mature.feeSats} sats"); // Claiming earlier, when the provider offers it. if (quote.instant is ClaimDepositQuote instant) { var instantBlocksToWait = instant.confirmationsRequired > quote.confirmations ? instant.confirmationsRequired - quote.confirmations : 0U; Console.WriteLine($"Or wait {instantBlocksToWait} blocks and " + $"pay {instant.feeSats} sats"); } ``` #### Javascript (Wasm) ```typescript const quote = await sdk.fetchClaimDepositQuote({ txid: deposit.txid, vout: deposit.vout }) // Claiming once the deposit matures, and how many blocks that is away. const blocksToWait = Math.max( 0, quote.mature.confirmationsRequired - quote.confirmations ) console.log(`Wait ${blocksToWait} blocks and pay ${quote.mature.feeSats} sats`) // Claiming earlier, when the provider offers it. if (quote.instant != null) { const instantBlocksToWait = Math.max( 0, quote.instant.confirmationsRequired - quote.confirmations ) console.log( `Or wait ${instantBlocksToWait} blocks and ` + `pay ${quote.instant.feeSats} sats` ) } ``` #### React Native ```typescript const quote = await sdk.fetchClaimDepositQuote({ txid: deposit.txid, vout: deposit.vout }) // Claiming once the deposit matures, and how many blocks that is away. const blocksToWait = Math.max(0, quote.mature.confirmationsRequired - quote.confirmations) console.log(`Wait ${blocksToWait} blocks and pay ${quote.mature.feeSats} sats`) // Claiming earlier, when the provider offers it. const instant = quote.instant if (instant != null) { const instantBlocks = Math.max(0, instant.confirmationsRequired - quote.confirmations) console.log(`Or wait ${instantBlocks} blocks and pay ${instant.feeSats} sats`) } ``` #### Flutter ```dart final request = FetchClaimDepositQuoteRequest( txid: deposit.txid, vout: deposit.vout, ); final quote = await sdk.fetchClaimDepositQuote(request: request); // Claiming once the deposit matures, and how many blocks that is away. final confirmations = quote.confirmations; final matureBlocks = quote.mature.confirmationsRequired > confirmations ? quote.mature.confirmationsRequired - confirmations : 0; print("Wait $matureBlocks blocks and pay ${quote.mature.feeSats} sats"); // Claiming earlier, when the provider offers it. final instant = quote.instant; if (instant != null) { final instantBlocks = instant.confirmationsRequired > confirmations ? instant.confirmationsRequired - confirmations : 0; print("Or wait $instantBlocks blocks and pay ${instant.feeSats} sats"); } ``` #### Python ```python try: request = FetchClaimDepositQuoteRequest(txid=deposit.txid, vout=deposit.vout) quote = await sdk.fetch_claim_deposit_quote(request=request) # Claiming once the deposit matures, and how many blocks that is away. blocks_to_wait = max( 0, quote.mature.confirmations_required - quote.confirmations ) logging.info( f"Wait {blocks_to_wait} blocks and pay {quote.mature.fee_sats} sats" ) # Claiming earlier, when the provider offers it. if quote.instant is not None: blocks_to_wait = max( 0, quote.instant.confirmations_required - quote.confirmations ) logging.info( f"Or wait {blocks_to_wait} blocks and pay {quote.instant.fee_sats} sats" ) except Exception as error: logging.error(error) raise ``` #### Go ```go quote, err := sdk.FetchClaimDepositQuote(breez_sdk_spark.FetchClaimDepositQuoteRequest{ Txid: deposit.Txid, Vout: deposit.Vout, }) if err != nil { return err } // Claiming once the deposit matures, and how many blocks that is away. blocksToWait := uint32(0) if quote.Mature.ConfirmationsRequired > quote.Confirmations { blocksToWait = quote.Mature.ConfirmationsRequired - quote.Confirmations } log.Printf("Wait %v blocks and pay %v sats", blocksToWait, quote.Mature.FeeSats) // Claiming earlier, when the provider offers it. if quote.Instant != nil { instantBlocks := uint32(0) if quote.Instant.ConfirmationsRequired > quote.Confirmations { instantBlocks = quote.Instant.ConfirmationsRequired - quote.Confirmations } log.Printf("Or wait %v blocks and pay %v sats", instantBlocks, quote.Instant.FeeSats) } ``` ## Listing unclaimed deposits Retrieve all deposits that have not yet been claimed. This includes pending deposits that do not yet have sufficient confirmations, as well as deposits with sufficient confirmations that failed to claim (with the specific failure reason). Pending deposits will be automatically claimed once they have sufficient confirmations, or sooner if the configured ceiling covers an early claim. A deposit claimed before maturity stays in the list with its `instant_claim_status` set to `InstantClaimStatus::Submitted` for a short time after submission, and is removed once the claim settles. When the SDK claims automatically it emits `SdkEvent::ClaimedDeposits` at submission, so a deposit can briefly appear both in that event and in this list. ### Rust ```rust let request = ListUnclaimedDepositsRequest {}; let response = sdk.list_unclaimed_deposits(request).await?; for deposit in response.deposits { info!("Unclaimed deposit: {}:{}", deposit.txid, deposit.vout); info!("Amount: {} sats", deposit.amount_sats); if let Some(claim_error) = &deposit.claim_error { match claim_error { DepositClaimError::MaxDepositClaimFeeExceeded { max_fee, required_fee_sats, required_fee_rate_sat_per_vbyte, .. } => { info!( "Max claim fee exceeded. Max: {:?}, Required: {} sats or {} sats/vByte", max_fee, required_fee_sats, required_fee_rate_sat_per_vbyte ); } DepositClaimError::MissingUtxo { .. } => { info!("UTXO not found when claiming deposit"); } DepositClaimError::Generic { message } => { info!("Claim failed: {}", message); } } } } ``` ### Swift ```swift let request = ListUnclaimedDepositsRequest() let response = try await sdk.listUnclaimedDeposits(request: request) for deposit in response.deposits { print("Unclaimed deposit: \(deposit.txid):\(deposit.vout)") print("Amount: \(deposit.amountSats) sats") if let claimError = deposit.claimError { switch claimError { case .maxDepositClaimFeeExceeded( let tx, let vout, let maxFee, let requiredFeeSats, let requiredFeeRateSatPerVbyte): let maxFeeStr: String if let maxFee = maxFee { switch maxFee { case .fixed(let amount): maxFeeStr = "\(amount) sats" case .rate(let satPerVbyte): maxFeeStr = "\(satPerVbyte) sats/vByte" } } else { maxFeeStr = "none" } print( "Max claim fee exceeded. Max: \(maxFeeStr), " + "Required: \(requiredFeeSats) sats or " + "\(requiredFeeRateSatPerVbyte) sats/vByte" ) case .missingUtxo(let tx, let vout): print("UTXO not found when claiming deposit") case .generic(let message): print("Claim failed: \(message)") } } } ``` ### Kotlin ```kotlin try { val request = ListUnclaimedDepositsRequest val response = sdk.listUnclaimedDeposits(request) for (deposit in response.deposits) { // Log.v("Breez", "Unclaimed deposit: ${deposit.txid}:${deposit.vout}") // Log.v("Breez", "Amount: ${deposit.amountSats} sats") deposit.claimError?.let { claimError -> when (claimError) { is DepositClaimError.MaxDepositClaimFeeExceeded -> { val maxFee = claimError.maxFee val maxFeeStr = when (maxFee) { is Fee.Fixed -> "${maxFee.amount} sats" is Fee.Rate -> "${maxFee.satPerVbyte} sats/vByte" null -> "none" } // Log.v("Breez", "Max claim fee exceeded. Max: $maxFeeStr, // Required: ${claimError.requiredFeeSats} sats or // ${claimError.requiredFeeRateSatPerVbyte} sats/vByte") } is DepositClaimError.MissingUtxo -> { // Log.v("Breez", "UTXO not found when claiming deposit") } is DepositClaimError.Generic -> { // Log.v("Breez", "Claim failed: ${claimError.message}") } } } } } catch (e: Exception) { // handle error } ``` ### C# ```csharp var request = new ListUnclaimedDepositsRequest(); var response = await sdk.ListUnclaimedDeposits(request: request); foreach (var deposit in response.deposits) { Console.WriteLine($"Unclaimed deposit: {deposit.txid}:{deposit.vout}"); Console.WriteLine($"Amount: {deposit.amountSats} sats"); if (deposit.claimError != null) { if (deposit.claimError is DepositClaimError.MaxDepositClaimFeeExceeded exceeded) { var maxFeeStr = "none"; if (exceeded.maxFee != null) { if (exceeded.maxFee is Fee.Fixed fixedFee) { maxFeeStr = $"{fixedFee.amount} sats"; } else if (exceeded.maxFee is Fee.Rate rateFee) { maxFeeStr = $"{rateFee.satPerVbyte} sats/vByte"; } } Console.WriteLine($"Claim failed: Fee exceeded. Max: {maxFeeStr}, " + $"Required: {exceeded.requiredFeeSats} sats or " + $"{exceeded.requiredFeeRateSatPerVbyte} sats/vByte"); } else if (deposit.claimError is DepositClaimError.MissingUtxo) { Console.WriteLine("Claim failed: UTXO not found"); } else if (deposit.claimError is DepositClaimError.Generic generic) { Console.WriteLine($"Claim failed: {generic.message}"); } } } ``` ### Javascript (Wasm) ```typescript const request: ListUnclaimedDepositsRequest = {} const response = await sdk.listUnclaimedDeposits(request) for (const deposit of response.deposits) { console.log(`Unclaimed deposit: ${deposit.txid}:${deposit.vout}`) console.log(`Amount: ${deposit.amountSats} sats`) if (deposit.claimError != null) { switch (deposit.claimError.type) { case 'maxDepositClaimFeeExceeded': { let maxFeeStr = 'none' if (deposit.claimError.maxFee != null) { if (deposit.claimError.maxFee.type === 'fixed') { maxFeeStr = `${deposit.claimError.maxFee.amount} sats` } else if (deposit.claimError.maxFee.type === 'rate') { maxFeeStr = `${deposit.claimError.maxFee.satPerVbyte} sats/vByte` } } console.log( `Max claim fee exceeded. Max: ${maxFeeStr}, ` + `Required: ${deposit.claimError.requiredFeeSats} sats or ` + `${deposit.claimError.requiredFeeRateSatPerVbyte} sats/vByte` ) break } case 'missingUtxo': console.log('UTXO not found when claiming deposit') break case 'generic': console.log(`Claim failed: ${deposit.claimError.message}`) break } } } ``` ### React Native ```typescript const request: ListUnclaimedDepositsRequest = {} const response = await sdk.listUnclaimedDeposits(request) for (const deposit of response.deposits) { console.log(`Unclaimed deposit: ${deposit.txid}:${deposit.vout}`) console.log(`Amount: ${deposit.amountSats} sats`) if (deposit.claimError != null) { if (deposit.claimError?.tag === DepositClaimError_Tags.MaxDepositClaimFeeExceeded) { let maxFeeStr = 'none' if (deposit.claimError.inner.maxFee != null) { if (deposit.claimError.inner.maxFee.tag === Fee_Tags.Fixed) { maxFeeStr = `${deposit.claimError.inner.maxFee.inner.amount} sats` } else if (deposit.claimError.inner.maxFee.tag === Fee_Tags.Rate) { maxFeeStr = `${deposit.claimError.inner.maxFee.inner.satPerVbyte} sats/vByte` } } console.log( `Max claim fee exceeded. Max: ${maxFeeStr}, Required: ${deposit.claimError.inner.requiredFeeSats} sats or ${deposit.claimError.inner.requiredFeeRateSatPerVbyte} sats/vByte` ) } else if (deposit.claimError?.tag === DepositClaimError_Tags.MissingUtxo) { console.log('UTXO not found when claiming deposit') } else if (deposit.claimError?.tag === DepositClaimError_Tags.Generic) { console.log(`Claim failed: ${deposit.claimError.inner.message}`) } } } ``` ### Flutter ```dart final request = ListUnclaimedDepositsRequest(); final response = await sdk.listUnclaimedDeposits(request: request); for (DepositInfo deposit in response.deposits) { print("Unclaimed deposit: ${deposit.txid}:${deposit.vout}"); print("Amount: ${deposit.amountSats} sats"); final claimError = deposit.claimError; if (claimError is DepositClaimError_MaxDepositClaimFeeExceeded) { final maxFeeStr = claimError.maxFee != null ? (claimError.maxFee is Fee_Fixed ? '${(claimError.maxFee as Fee_Fixed).amount} sats' : '${(claimError.maxFee as Fee_Rate).satPerVbyte} sats/vByte') : 'none'; print("Max claim fee exceeded. Max: $maxFeeStr, " "Required: ${claimError.requiredFeeSats} sats or " "${claimError.requiredFeeRateSatPerVbyte} sats/vByte"); } else if (claimError is DepositClaimError_MissingUtxo) { print("UTXO not found when claiming deposit"); } else if (claimError is DepositClaimError_Generic) { print("Claim failed: ${claimError.message}"); } } ``` ### Python ```python try: request = ListUnclaimedDepositsRequest() response = await sdk.list_unclaimed_deposits(request=request) for deposit in response.deposits: logging.info(f"Unclaimed deposit: {deposit.txid}:{deposit.vout}") logging.info(f"Amount: {deposit.amount_sats} sats") if deposit.claim_error: if isinstance( deposit.claim_error, DepositClaimError.MAX_DEPOSIT_CLAIM_FEE_EXCEEDED ): max_fee_str = "none" if deposit.claim_error.max_fee is not None: if isinstance(deposit.claim_error.max_fee, Fee.FIXED): max_fee_str = f"{deposit.claim_error.max_fee.amount} sats" elif isinstance(deposit.claim_error.max_fee, Fee.RATE): max_fee_str = f"{deposit.claim_error.max_fee.sat_per_vbyte} sats/vByte" logging.info( f"Claim failed: Fee exceeded. Max: {max_fee_str}, " f"Required: {deposit.claim_error.required_fee_sats} sats " f"or {deposit.claim_error.required_fee_rate_sat_per_vbyte} sats/vByte" ) elif isinstance(deposit.claim_error, DepositClaimError.MISSING_UTXO): logging.info("Claim failed: UTXO not found") elif isinstance(deposit.claim_error, DepositClaimError.GENERIC): logging.info(f"Claim failed: {deposit.claim_error.message}") except Exception as error: logging.error(error) raise ``` ### Go ```go request := breez_sdk_spark.ListUnclaimedDepositsRequest{} response, err := sdk.ListUnclaimedDeposits(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } for _, deposit := range response.Deposits { log.Printf("Unclaimed Deposit: %v:%v", deposit.Txid, deposit.Vout) log.Printf("Amount: %v sats", deposit.AmountSats) if claimErr := *deposit.ClaimError; claimErr != nil { switch claimErr := claimErr.(type) { case breez_sdk_spark.DepositClaimErrorMaxDepositClaimFeeExceeded: maxFeeStr := "none" if claimErr.MaxFee != nil { switch fee := (*claimErr.MaxFee).(type) { case breez_sdk_spark.FeeFixed: maxFeeStr = fmt.Sprintf("%v sats", fee.Amount) case breez_sdk_spark.FeeRate: maxFeeStr = fmt.Sprintf("%v sats/vByte", fee.SatPerVbyte) } } log.Printf( "Max claim fee exceeded. Max: %v, Required: %v sats or %v sats/vByte", maxFeeStr, claimErr.RequiredFeeSats, claimErr.RequiredFeeRateSatPerVbyte, ) case breez_sdk_spark.DepositClaimErrorMissingUtxo: log.Print("UTXO not found when claiming deposit") case breez_sdk_spark.DepositClaimErrorGeneric: log.Printf("Claim failed: %v", claimErr.Message) } } } ``` ## Refunding deposits When a deposit cannot be successfully claimed you can refund it to an external Bitcoin address. This creates a transaction that sends the amount (minus transaction fees) to the specified destination address. The [recommended fees](#recommended-fees) API is useful for determining appropriate fee levels for refund transactions. A deposit can only be refunded once it has enough confirmations. Calling `refund_deposit` earlier fails, reporting the deposit as unknown while it is unconfirmed and as having too few confirmations for a block or so after that. Nothing is signed or stored when this happens, so retry after a few more blocks. ### Rust ```rust let txid = "your_deposit_txid".to_string(); let vout = 0; let destination_address = "bc1qexample...".to_string(); // Your Bitcoin address // Set the fee for the refund transaction using the half-hour feerate let recommended_fees = sdk.recommended_fees().await?; let fee = Fee::Rate { sat_per_vbyte: recommended_fees.half_hour_fee, }; // or using a fixed amount //let fee = Fee::Fixed { amount: 500 }; // let request = RefundDepositRequest { txid, vout, destination_address, fee, }; let response = sdk.refund_deposit(request).await?; info!("Refund transaction created:"); info!("Transaction ID: {}", response.tx_id); info!("Transaction hex: {}", response.tx_hex); ``` ### Swift ```swift let txid = "your_deposit_txid" let vout: UInt32 = 0 let destinationAddress = "bc1qexample..." // Your Bitcoin address // Set the fee for the refund transaction using the half-hour feerate let recommendedFees = try await sdk.recommendedFees() let fee = Fee.rate(satPerVbyte: recommendedFees.halfHourFee) // or using a fixed amount //let fee = Fee.fixed(amount: 500) // 500 sats // let request = RefundDepositRequest( txid: txid, vout: vout, destinationAddress: destinationAddress, fee: fee ) let response = try await sdk.refundDeposit(request: request) print("Refund transaction created:") print("Transaction ID: \(response.txId)") print("Transaction hex: \(response.txHex)") ``` ### Kotlin ```kotlin try { val txid = "your_deposit_txid" val vout = 0u val destinationAddress = "bc1qexample..." // Your Bitcoin address // Set the fee for the refund transaction using the half-hour feerate val recommendedFees = sdk.recommendedFees() val fee = Fee.Rate(recommendedFees.halfHourFee) // or using a fixed amount //val fee = Fee.Fixed(500u) // val request = RefundDepositRequest( txid = txid, vout = vout, destinationAddress = destinationAddress, fee = fee ) val response = sdk.refundDeposit(request) // Log.v("Breez", "Refund transaction created:") // Log.v("Breez", "Transaction ID: ${response.txId}") // Log.v("Breez", "Transaction hex: ${response.txHex}") } catch (e: Exception) { // handle error } ``` ### C# ```csharp var txid = "your_deposit_txid"; var vout = 0U; var destinationAddress = "bc1qexample..."; // Your Bitcoin address // Set the fee for the refund transaction using the half-hour feerate var recommendedFees = await sdk.RecommendedFees(); var fee = new Fee.Rate(satPerVbyte: recommendedFees.halfHourFee); // or using a fixed amount //var fee = new Fee.Fixed(amount: 500); // var request = new RefundDepositRequest( txid: txid, vout: vout, destinationAddress: destinationAddress, fee: fee ); var response = await sdk.RefundDeposit(request: request); Console.WriteLine("Refund transaction created:"); Console.WriteLine($"Transaction ID: {response.txId}"); Console.WriteLine($"Transaction hex: {response.txHex}"); ``` ### Javascript (Wasm) ```typescript const txid = 'your_deposit_txid' const vout = 0 const destinationAddress = 'bc1qexample...' // Your Bitcoin address // Set the fee for the refund transaction using the half-hour feerate const recommendedFees = await sdk.recommendedFees() const fee: Fee = { type: 'rate', satPerVbyte: recommendedFees.halfHourFee } // or using a fixed amount // const fee: Fee = { type: 'fixed', amount: 500 } // const request: RefundDepositRequest = { txid, vout, destinationAddress, fee } const response = await sdk.refundDeposit(request) console.log('Refund transaction created:') console.log('Transaction ID:', response.txId) console.log('Transaction hex:', response.txHex) ``` ### React Native ```typescript const txid = 'your_deposit_txid' const vout = 0 const destinationAddress = 'bc1qexample...' // Your Bitcoin address // Set the fee for the refund transaction using the half-hour feerate const recommendedFees = await sdk.recommendedFees() const fee = new Fee.Rate({ satPerVbyte: recommendedFees.halfHourFee }) // or using a fixed amount // const fee = new Fee.Fixed({ amount: BigInt(500) }) // const request: RefundDepositRequest = { txid, vout, destinationAddress, fee } const response = await sdk.refundDeposit(request) console.log('Refund transaction created:') console.log('Transaction ID:', response.txId) console.log('Transaction hex:', response.txHex) ``` ### Flutter ```dart String txid = "your_deposit_txid"; int vout = 0; String destinationAddress = "bc1qexample..."; // Your Bitcoin address // Set the fee for the refund transaction using the half-hour feerate final recommendedFees = await sdk.recommendedFees(); Fee fee = Fee.rate(satPerVbyte: recommendedFees.halfHourFee); // or using a fixed amount //Fee fee = Fee.fixed(amount: BigInt.from(500)); // final request = RefundDepositRequest( txid: txid, vout: vout, destinationAddress: destinationAddress, fee: fee, ); final response = await sdk.refundDeposit(request: request); print("Refund transaction created:"); print("Transaction ID: ${response.txId}"); print("Transaction hex: ${response.txHex}"); ``` ### Python ```python try: txid = "your_deposit_txid" vout = 0 destination_address = "bc1qexample..." # Your Bitcoin address # Set the fee for the refund transaction using the half-hour feerate recommended_fees = await sdk.recommended_fees() fee = Fee.RATE(sat_per_vbyte=recommended_fees.half_hour_fee) # or using a fixed amount #fee = Fee.FIXED(amount=500) # request = RefundDepositRequest( txid=txid, vout=vout, destination_address=destination_address, fee=fee ) response = await sdk.refund_deposit(request=request) logging.info("Refund transaction created:") logging.info(f"Transaction ID: {response.tx_id}") logging.info(f"Transaction hex: {response.tx_hex}") except Exception as error: logging.error(error) raise ``` ### Go ```go txid := "" vout := uint32(0) destinationAddress := "bc1qexample..." // Your Bitcoin address // Set the fee for the refund transaction using the half-hour feerate recommendedFees, err := sdk.RecommendedFees() if err != nil { return err } fee := breez_sdk_spark.Fee(breez_sdk_spark.FeeRate{SatPerVbyte: recommendedFees.HalfHourFee}) // or using a fixed amount //fee := breez_sdk_spark.Fee(breez_sdk_spark.FeeFixed{Amount: 500}) // request := breez_sdk_spark.RefundDepositRequest{ Txid: txid, Vout: vout, DestinationAddress: destinationAddress, Fee: fee, } response, err := sdk.RefundDeposit(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } log.Print("Refund transaction created:") log.Printf("Transaction ID: %v", response.TxId) log.Printf("Transaction hex: %v", response.TxHex) ``` **Developer note** The total fee must cover at least 1 sat/vB of the refund transaction so it can be relayed by the Bitcoin network. The exact minimum depends on the size of the transaction, which varies with the destination address type: around 99 sats to a native segwit address and 111 sats to a taproot one. If the fee is lower, the refund request is rejected and the error states the required minimum. ### Tracking a refund `refund_state` on `DepositInfo` reports how far the refund has got: - **`RefundState::BroadcastPending`**: the refund is signed and stored but has not been seen on the network. The SDK rebroadcasts it on every sync until the deposit is spent, so a refund that failed to send because of a temporary network problem recovers on its own. - **`RefundState::Broadcast`**: the network has accepted the refund and it is waiting to confirm. The deposit disappears from `list_unclaimed_deposits` once it does. A refund created near the 1 sat/vB minimum can stay at `RefundState::BroadcastPending` indefinitely if the network's minimum relay fee later rises above what it pays. Rebroadcasting cannot fix this, because the network keeps refusing the same transaction. Read `last_error` for the reason the network gave, then call `refund_deposit` again at a higher fee to replace it. Replacing a refund that is already on the network costs more than the original fee, because the replacement also pays to relay its own size. When the fee offered is too low, the call is rejected and the error states the minimum required. ## Implementing a custom claim logic For advanced use cases, you may want to implement a custom claim logic instead of relying on the SDK's automatic process. This gives you complete control over when and how deposits are claimed. To disable automatic claims, unset the [maximum deposit claim fee](/guide/config.md#max-deposit-claim-fee). Then use the methods described above to manually claim deposits based on your business logic. Common scenarios for custom claiming logic include: - **Dynamic fee adjustment**: Adjust claiming fees based on market conditions or priority - **Conditional claiming**: Only claim deposits that meet certain criteria (amount thresholds, time windows, etc.) - **Integration with external systems**: Coordinate claims with other business processes The [recommended fees](#recommended-fees) API is useful for determining appropriate fee levels for claiming deposits. For example, you can implement a custom claim logic to only claim deposits if the required fee rate is less than the fastest recommended fee (or any other). ### Rust ```rust if let Some(DepositClaimError::MaxDepositClaimFeeExceeded { required_fee_rate_sat_per_vbyte, .. }) = &deposit.claim_error { let recommended_fees = sdk.recommended_fees().await?; if *required_fee_rate_sat_per_vbyte <= recommended_fees.fastest_fee { let request = ClaimDepositRequest { txid: deposit.txid.clone(), vout: deposit.vout, max_fee: Some(MaxFee::Rate { sat_per_vbyte: *required_fee_rate_sat_per_vbyte, }), }; sdk.claim_deposit(request).await?; } } ``` ### Swift ```swift if case .maxDepositClaimFeeExceeded(_, _, _, _, let requiredFeeRateSatPerVbyte) = deposit.claimError { let recommendedFees = try await sdk.recommendedFees() if requiredFeeRateSatPerVbyte <= recommendedFees.fastestFee { let claimRequest = ClaimDepositRequest( txid: deposit.txid, vout: deposit.vout, maxFee: MaxFee.rate(satPerVbyte: requiredFeeRateSatPerVbyte) ) try await sdk.claimDeposit(request: claimRequest) } } ``` ### Kotlin ```kotlin try { val claimError = deposit.claimError if (claimError is DepositClaimError.MaxDepositClaimFeeExceeded) { val requiredFeeRate = claimError.requiredFeeRateSatPerVbyte val recommendedFees = sdk.recommendedFees() if (requiredFeeRate <= recommendedFees.fastestFee) { val claimRequest = ClaimDepositRequest( txid = deposit.txid, vout = deposit.vout, maxFee = MaxFee.Rate(requiredFeeRate) ) sdk.claimDeposit(claimRequest) } } } catch (e: Exception) { // handle error } ``` ### C# ```csharp if (deposit.claimError is DepositClaimError.MaxDepositClaimFeeExceeded exceeded) { var requiredFeeRate = exceeded.requiredFeeRateSatPerVbyte; var recommendedFees = await sdk.RecommendedFees(); if (requiredFeeRate <= recommendedFees.fastestFee) { var claimRequest = new ClaimDepositRequest( txid: deposit.txid, vout: deposit.vout, maxFee: new MaxFee.Rate(satPerVbyte: requiredFeeRate) ); await sdk.ClaimDeposit(request: claimRequest); } } ``` ### Javascript (Wasm) ```typescript if (deposit.claimError?.type === 'maxDepositClaimFeeExceeded') { const requiredFeeRate = deposit.claimError.requiredFeeRateSatPerVbyte const recommendedFees = await sdk.recommendedFees() if (requiredFeeRate <= recommendedFees.fastestFee) { const claimRequest: ClaimDepositRequest = { txid: deposit.txid, vout: deposit.vout, maxFee: { type: 'rate', satPerVbyte: requiredFeeRate } } await sdk.claimDeposit(claimRequest) } } ``` ### React Native ```typescript if (deposit.claimError?.tag === DepositClaimError_Tags.MaxDepositClaimFeeExceeded) { const requiredFeeRate = deposit.claimError.inner.requiredFeeRateSatPerVbyte const recommendedFees = await sdk.recommendedFees() if (requiredFeeRate <= recommendedFees.fastestFee) { const claimRequest: ClaimDepositRequest = { txid: deposit.txid, vout: deposit.vout, maxFee: new MaxFee.Rate({ satPerVbyte: requiredFeeRate }) } await sdk.claimDeposit(claimRequest) } } ``` ### Flutter ```dart final claimError = deposit.claimError; if (claimError is DepositClaimError_MaxDepositClaimFeeExceeded) { final requiredFeeRate = claimError.requiredFeeRateSatPerVbyte; final recommendedFees = await sdk.recommendedFees(); if (requiredFeeRate <= recommendedFees.fastestFee) { final claimRequest = ClaimDepositRequest( txid: deposit.txid, vout: deposit.vout, maxFee: MaxFee.rate(satPerVbyte: requiredFeeRate), ); await sdk.claimDeposit(request: claimRequest); } } ``` ### Python ```python try: if isinstance( deposit.claim_error, DepositClaimError.MAX_DEPOSIT_CLAIM_FEE_EXCEEDED ): required_fee_rate = deposit.claim_error.required_fee_rate_sat_per_vbyte recommended_fees = await sdk.recommended_fees() if required_fee_rate <= recommended_fees.fastest_fee: claim_request = ClaimDepositRequest( txid=deposit.txid, vout=deposit.vout, max_fee=MaxFee.RATE(sat_per_vbyte=required_fee_rate), ) await sdk.claim_deposit(request=claim_request) except Exception as error: logging.error(error) raise ``` ### Go ```go if claimErr := *deposit.ClaimError; claimErr != nil { if exceeded, ok := claimErr.(breez_sdk_spark.DepositClaimErrorMaxDepositClaimFeeExceeded); ok { requiredFeeRate := exceeded.RequiredFeeRateSatPerVbyte recommendedFees, err := sdk.RecommendedFees() if err != nil { return err } if requiredFeeRate <= recommendedFees.FastestFee { maxFee := breez_sdk_spark.MaxFee(breez_sdk_spark.MaxFeeRate{SatPerVbyte: requiredFeeRate}) claimRequest := breez_sdk_spark.ClaimDepositRequest{ Txid: deposit.Txid, Vout: deposit.Vout, MaxFee: &maxFee, } _, err := sdk.ClaimDeposit(claimRequest) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } } } } ``` ## Recommended fees Get Bitcoin fee estimates for different confirmation targets to help determine appropriate fee levels for claiming or refunding deposits. ### Rust ```rust let response = sdk.recommended_fees().await?; info!("Fastest fee: {} sats/vByte", response.fastest_fee); info!("Half-hour fee: {} sats/vByte", response.half_hour_fee); info!("Hour fee: {} sats/vByte", response.hour_fee); info!("Economy fee: {} sats/vByte", response.economy_fee); info!("Minimum fee: {} sats/vByte", response.minimum_fee); ``` ### Swift ```swift let response = try await sdk.recommendedFees() print("Fastest fee: \(response.fastestFee) sats/vByte") print("Half-hour fee: \(response.halfHourFee) sats/vByte") print("Hour fee: \(response.hourFee) sats/vByte") print("Economy fee: \(response.economyFee) sats/vByte") print("Minimum fee: \(response.minimumFee) sats/vByte") ``` ### Kotlin ```kotlin val response = sdk.recommendedFees() println("Fastest fee: ${response.fastestFee} sats/vByte") println("Half-hour fee: ${response.halfHourFee} sats/vByte") println("Hour fee: ${response.hourFee} sats/vByte") println("Economy fee: ${response.economyFee} sats/vByte") println("Minimum fee: ${response.minimumFee} sats/vByte") ``` ### C# ```csharp var response = await sdk.RecommendedFees(); Console.WriteLine($"Fastest fee: {response.fastestFee} sats/vByte"); Console.WriteLine($"Half-hour fee: {response.halfHourFee} sats/vByte"); Console.WriteLine($"Hour fee: {response.hourFee} sats/vByte"); Console.WriteLine($"Economy fee: {response.economyFee} sats/vByte"); Console.WriteLine($"Minimum fee: {response.minimumFee} sats/vByte"); } ``` ### Javascript (Wasm) ```typescript const response = await sdk.recommendedFees() console.log('Fastest fee:', response.fastestFee, 'sats/vByte') console.log('Half-hour fee:', response.halfHourFee, 'sats/vByte') console.log('Hour fee:', response.hourFee, 'sats/vByte') console.log('Economy fee:', response.economyFee, 'sats/vByte') console.log('Minimum fee:', response.minimumFee, 'sats/vByte') ``` ### React Native ```typescript const response = await sdk.recommendedFees() console.log('Fastest fee:', response.fastestFee, 'sats/vByte') console.log('Half-hour fee:', response.halfHourFee, 'sats/vByte') console.log('Hour fee:', response.hourFee, 'sats/vByte') console.log('Economy fee:', response.economyFee, 'sats/vByte') console.log('Minimum fee:', response.minimumFee, 'sats/vByte') ``` ### Flutter ```dart final response = await sdk.recommendedFees(); print("Fastest fee: ${response.fastestFee} sats/vByte"); print("Half-hour fee: ${response.halfHourFee} sats/vByte"); print("Hour fee: ${response.hourFee} sats/vByte"); print("Economy fee: ${response.economyFee} sats/vByte"); print("Minimum fee: ${response.minimumFee} sats/vByte"); ``` ### Python ```python response = await sdk.recommended_fees() logging.info(f"Fastest fee: {response.fastest_fee} sats/vByte") logging.info(f"Half-hour fee: {response.half_hour_fee} sats/vByte") logging.info(f"Hour fee: {response.hour_fee} sats/vByte") logging.info(f"Economy fee: {response.economy_fee} sats/vByte") logging.info(f"Minimum fee: {response.minimum_fee} sats/vByte") ``` ### Go ```go response, err := sdk.RecommendedFees() if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } log.Printf("Fastest fee: %v sats/vByte", response.FastestFee) log.Printf("Half-hour fee: %v sats/vByte", response.HalfHourFee) log.Printf("Hour fee: %v sats/vByte", response.HourFee) log.Printf("Economy fee: %v sats/vByte", response.EconomyFee) log.Printf("Minimum fee: %v sats/vByte", response.MinimumFee) ``` --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Using LNURL and Lightning addresses](/guide/lnurl.md)** # Using LNURL and Lightning addresses The Breez SDK - Spark supports the following LNURL functionality: - **[Sending payments using LNURL-Pay/Lightning address]** (including BIP353 addresses) - **[Managing contacts]** — Save frequently used Lightning addresses for quick access - **[Receiving payments using LNURL-Pay/Lightning address]** - **[Receiving payments using LNURL-Withdraw]** - **[Using LNURL-Auth]** - **[LNURL-Verify]** — Payment verification via [LUD-21](https://github.com/lnurl/luds/blob/luds/21.md) and Nostr Zap receipts via [NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md). [Sending payments using LNURL-Pay/Lightning address]: lnurl_pay.md [Receiving payments using LNURL-Pay/Lightning address]: receive_lnurl_pay.md [Receiving payments using LNURL-Withdraw]: lnurl_withdraw.md [Using LNURL-Auth]: lnurl_auth.md [LNURL-Verify]: receive_lnurl_pay.md#payment-verification-lud-21 [Managing contacts]: contacts.md **→ [Sending payments using LNURL-Pay/Lightning address](/guide/lnurl_pay.md)** # Sending payments using LNURL-Pay and Lightning address ## Preparing LNURL Payments API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.prepare_lnurl_pay During the prepare step, the SDK ensures that the inputs are valid with respect to the LNURL-pay request, and also returns the fees related to the payment so they can be confirmed. Payments can be sent without holding Bitcoin by converting on-the-fly as a step before sending a payment. See Converting tokens for more information. ### Setting the receiver amount When you want the payment recipient to receive a specific amount. #### Rust ```rust // Endpoint can also be of the form: // lnurlp://domain.com/lnurl-pay?key=val // lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttsv9un7um9wdekjmmw84jxywf5x43rvv35xgmr2enrxanr2cfcvsmnwe3jxcukvde48qukgdec89snwde3vfjxvepjxpjnjvtpxd3kvdnxx5crxwpjvyunsephsz36jf let lnurl_pay_url = "lightning@address.com"; if let Ok(InputType::LightningAddress(details)) = sdk.parse(lnurl_pay_url).await { let amount_sats = 5_000; let optional_comment = Some("".to_string()); let optional_validate_success_action_url = Some(true); let prepare_response = sdk .prepare_lnurl_pay(PrepareLnurlPayRequest { amount: amount_sats, pay_request: details.pay_request, comment: optional_comment, validate_success_action_url: optional_validate_success_action_url, token_identifier: None, conversion_options: None, fee_policy: None, }) .await?; // If the fees are acceptable, continue to create the LNURL Pay let fee_sats = prepare_response.fee_sats; info!("Fees: {fee_sats} sats"); } ``` #### Swift ```swift // Endpoint can also be of the form: // lnurlp://domain.com/lnurl-pay?key=val // lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttsv9un7um9wdekjmmw84jxywf5x43rvv35xgmr2enrxanr2cfcvsmnwe3jxcukvde48qukgdec89snwde3vfjxvepjxpjnjvtpxd3kvdnxx5crxwpjvyunsephsz36jf let lnurlPayUrl = "lightning@address.com" let inputType = try await sdk.parse(input: lnurlPayUrl) if case .lightningAddress(v1: let details) = inputType { let amountSats = BInt(5_000) let optionalComment = "" let payRequest = details.payRequest let optionalValidateSuccessActionUrl = true let request = PrepareLnurlPayRequest( amount: amountSats, payRequest: payRequest, comment: optionalComment, validateSuccessActionUrl: optionalValidateSuccessActionUrl, tokenIdentifier: nil, conversionOptions: nil, feePolicy: nil ) let prepareResponse = try await sdk.prepareLnurlPay(request: request) // If the fees are acceptable, continue to create the LNURL Pay let feeSats = prepareResponse.feeSats print("Fees: \(feeSats) sats") } ``` #### Kotlin ```kotlin // Endpoint can also be of the form: // lnurlp://domain.com/lnurl-pay?key=val // lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttsv9un7um9wdekjmmw84jxywf5x43rvv35xgmr2enrxanr2cfcvsmnwe3jxcukvde48qukgdec89snwde3vfjxvepjxpjnjvtpxd3kvdnxx5crxwpjvyunsephsz36jf val lnurlPayUrl = "lightning@address.com" try { val inputType = sdk.parse(lnurlPayUrl) if (inputType is InputType.LightningAddress) { val amountSats = BigInteger.fromLong(5_000L) val optionalComment = "" val payRequest = inputType.v1.payRequest val optionalValidateSuccessActionUrl = true val req = PrepareLnurlPayRequest( amount = amountSats, payRequest = payRequest, comment = optionalComment, validateSuccessActionUrl = optionalValidateSuccessActionUrl, tokenIdentifier = null, conversionOptions = null, feePolicy = null, ) val prepareResponse = sdk.prepareLnurlPay(req) // If the fees are acceptable, continue to create the LNURL Pay val feeSats = prepareResponse.feeSats // Log.v("Breez", "Fees: ${feeSats} sats") } } catch (e: Exception) { // handle error } ``` #### C# ```csharp // Endpoint can also be of the form: // lnurlp://domain.com/lnurl-pay?key=val // lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttsv9un7um9wdekjmmw84jxywf5x43r // vv35xgmr2enrxanr2cfcvsmnwe3jxcukvde48qukgdec89snwde3vfjxvepjxpjnjvtpxd3k // vdnxx5crxwpjvyunsephsz36jf var lnurlPayUrl = "lightning@address.com"; var parsedInput = await sdk.Parse(lnurlPayUrl); if (parsedInput is InputType.LightningAddress lightningAddress) { var details = lightningAddress.v1; var amountSats = 5_000UL; var optionalComment = ""; var payRequest = details.payRequest; var optionalValidateSuccessActionUrl = true; var request = new PrepareLnurlPayRequest( amount: amountSats, payRequest: payRequest, comment: optionalComment, validateSuccessActionUrl: optionalValidateSuccessActionUrl, tokenIdentifier: null, conversionOptions: null, feePolicy: null ); var prepareResponse = await sdk.PrepareLnurlPay(request: request); // If the fees are acceptable, continue to create the LNURL Pay var feeSats = prepareResponse.feeSats; Console.WriteLine($"Fees: {feeSats} sats"); } ``` #### Javascript (Wasm) ```typescript // Endpoint can also be of the form: // lnurlp://domain.com/lnurl-pay?key=val // lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttsv9un7um9wdekjmmw84jxywf5x43rvv35xgmr2enrxanr2cfcvsmnwe3jxcukvde48qukgdec89snwde3vfjxvepjxpjnjvtpxd3kvdnxx5crxwpjvyunsephsz36jf const lnurlPayUrl = 'lightning@address.com' const input = await sdk.parse(lnurlPayUrl) if (input.type === 'lightningAddress') { const amountSats = BigInt(5_000) const optionalComment = '' const payRequest = input.payRequest const optionalValidateSuccessActionUrl = true const prepareResponse = await sdk.prepareLnurlPay({ amount: amountSats, payRequest, comment: optionalComment, validateSuccessActionUrl: optionalValidateSuccessActionUrl, tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: undefined }) // If the fees are acceptable, continue to create the LNURL Pay const feeSats = prepareResponse.feeSats console.log(`Fees: ${feeSats} sats`) } ``` #### React Native ```typescript // Endpoint can also be of the form: // lnurlp://domain.com/lnurl-pay?key=val // lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttsv9un7um9wdekjmmw84jxywf5x43rvv35xgmr2enrxanr2cfcvsmnwe3jxcukvde48qukgdec89snwde3vfjxvepjxpjnjvtpxd3kvdnxx5crxwpjvyunsephsz36jf const lnurlPayUrl = 'lightning@address.com' const input = await sdk.parse(lnurlPayUrl) if (input.tag === InputType_Tags.LightningAddress) { const amountSats = BigInt(5_000) const optionalComment = '' const payRequest = input.inner[0].payRequest const optionalValidateSuccessActionUrl = true const prepareResponse = await sdk.prepareLnurlPay({ amount: amountSats, payRequest, comment: optionalComment, validateSuccessActionUrl: optionalValidateSuccessActionUrl, tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: undefined }) // If the fees are acceptable, continue to create the LNURL Pay const feeSats = prepareResponse.feeSats console.log(`Fees: ${feeSats} sats`) } ``` #### Flutter ```dart // Endpoint can also be of the form: // lnurlp://domain.com/lnurl-pay?key=val // lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttsv9un7um9wdekjmmw84jxywf5x43rvv35xgmr2enrxanr2cfcvsmnwe3jxcukvde48qukgdec89snwde3vfjxvepjxpjnjvtpxd3kvdnxx5crxwpjvyunsephsz36jf String lnurlPayUrl = "lightning@address.com"; InputType inputType = await sdk.parse(input: lnurlPayUrl); if (inputType is InputType_LightningAddress) { BigInt amountSats = BigInt.from(5000); String optionalComment = ""; bool optionalValidateSuccessActionUrl = true; PrepareLnurlPayRequest request = PrepareLnurlPayRequest( amount: amountSats, payRequest: inputType.field0.payRequest, comment: optionalComment, validateSuccessActionUrl: optionalValidateSuccessActionUrl, tokenIdentifier: null, conversionOptions: null, feePolicy: null, ); PrepareLnurlPayResponse prepareResponse = await sdk.prepareLnurlPay(request: request); // If the fees are acceptable, continue to create the LNURL Pay BigInt feeSats = prepareResponse.feeSats; print("Fees: $feeSats sats"); } ``` #### Python ```python # Endpoint can also be of the form: # lnurlp://domain.com/lnurl-pay?key=val # lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttsv9un7um9wdekjmmw84jxywf5x43r # vv35xgmr2enrxanr2cfcvsmnwe3jxcukvde48qukgdec89snwde3vfjxvepjxpjnjvtpxd3k # vdnxx5crxwpjvyunsephsz36jf lnurl_pay_url = "lightning@address.com" try: parsed_input = await sdk.parse(lnurl_pay_url) if isinstance(parsed_input, InputType.LIGHTNING_ADDRESS): details = parsed_input[0] amount_sats = 5_000 optional_comment = "" pay_request = details.pay_request optional_validate_success_action_url = True request = PrepareLnurlPayRequest( amount=amount_sats, pay_request=pay_request, comment=optional_comment, validate_success_action_url=optional_validate_success_action_url, token_identifier=None, conversion_options=None, fee_policy=None, ) prepare_response = await sdk.prepare_lnurl_pay(request=request) # If the fees are acceptable, continue to create the LNURL Pay logging.debug(f"Fees: {prepare_response.fee_sats} sats") return prepare_response except Exception as error: logging.error(error) raise ``` #### Go ```go // Endpoint can also be of the form: // lnurlp://domain.com/lnurl-pay?key=val // lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4excttsv9un7um9wdekjmmw84jxywf5x43rvv35xgmr2enrxanr2cfcvsmnwe3jxcukvde48qukgdec89snwde3vfjxvepjxpjnjvtpxd3kvdnxx5crxwpjvyunsephsz36jf lnurlPayUrl := "lightning@address.com" input, err := sdk.Parse(lnurlPayUrl) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } switch inputType := input.(type) { case breez_sdk_spark.InputTypeLightningAddress: amountSats := new(big.Int).SetInt64(5_000) optionalComment := "" optionalValidateSuccessActionUrl := true request := breez_sdk_spark.PrepareLnurlPayRequest{ Amount: amountSats, PayRequest: inputType.Field0.PayRequest, Comment: &optionalComment, ValidateSuccessActionUrl: &optionalValidateSuccessActionUrl, TokenIdentifier: nil, ConversionOptions: nil, FeePolicy: nil, } prepareResponse, err := sdk.PrepareLnurlPay(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } // If the fees are acceptable, continue to create the LNURL Pay feeSats := prepareResponse.FeeSats log.Printf("Fees: %v sats", feeSats) return &prepareResponse, nil } ``` ### Setting the fee policy By default, fees are added on top of the amount (`FeePolicy::FeesExcluded`). Use `FeePolicy::FeesIncluded` to deduct fees from the amount instead—the receiver gets the amount minus fees. This is particularly useful when you want to spend your entire balance in a single payment—simply provide your full balance as the amount. #### Rust ```rust // By default (FeePolicy::FeesExcluded), fees are added on top of the amount. // Use FeePolicy::FeesIncluded to deduct fees from the amount instead. // The receiver gets amount minus fees. let optional_comment = Some("".to_string()); let optional_validate_success_action_url = Some(true); let amount_sats = 5_000; let prepare_response = sdk .prepare_lnurl_pay(PrepareLnurlPayRequest { amount: amount_sats, pay_request, comment: optional_comment, validate_success_action_url: optional_validate_success_action_url, token_identifier: None, conversion_options: None, fee_policy: Some(FeePolicy::FeesIncluded), }) .await?; // If the fees are acceptable, continue to create the LNURL Pay let fee_sats = prepare_response.fee_sats; info!("Fees: {fee_sats} sats"); // The receiver gets amount_sats - fee_sats ``` #### Swift ```swift // By default (.feesExcluded), fees are added on top of the amount. // Use .feesIncluded to deduct fees from the amount instead. // The receiver gets amount minus fees. let amountSats = BInt(5_000) let optionalComment = "" let optionalValidateSuccessActionUrl = true let request = PrepareLnurlPayRequest( amount: amountSats, payRequest: payRequest, comment: optionalComment, validateSuccessActionUrl: optionalValidateSuccessActionUrl, tokenIdentifier: nil, conversionOptions: nil, feePolicy: .feesIncluded ) let response = try await sdk.prepareLnurlPay(request: request) // If the fees are acceptable, continue to create the LNURL Pay let feeSats = response.feeSats print("Fees: \(feeSats) sats") // The receiver gets amountSats - feeSats ``` #### Kotlin ```kotlin // By default (FeePolicy.FEES_EXCLUDED), fees are added on top of the amount. // Use FeePolicy.FEES_INCLUDED to deduct fees from the amount instead. // The receiver gets amount minus fees. val optionalComment = "" val optionalValidateSuccessActionUrl = true val amountSats = BigInteger.fromLong(5_000L) val req = PrepareLnurlPayRequest( amount = amountSats, payRequest = payRequest, comment = optionalComment, validateSuccessActionUrl = optionalValidateSuccessActionUrl, tokenIdentifier = null, conversionOptions = null, feePolicy = FeePolicy.FEES_INCLUDED, ) val prepareResponse = sdk.prepareLnurlPay(req) // If the fees are acceptable, continue to create the LNURL Pay val feeSats = prepareResponse.feeSats // Log.v("Breez", "Fees: ${feeSats} sats") // The receiver gets amountSats - feeSats ``` #### C# ```csharp // By default (FeePolicy.FeesExcluded), fees are added on top of the amount. // Use FeePolicy.FeesIncluded to deduct fees from the amount instead. // The receiver gets amount minus fees. var amountSats = 5_000UL; var optionalComment = ""; var optionalValidateSuccessActionUrl = true; var request = new PrepareLnurlPayRequest( amount: amountSats, payRequest: payRequest, comment: optionalComment, validateSuccessActionUrl: optionalValidateSuccessActionUrl, tokenIdentifier: null, conversionOptions: null, feePolicy: FeePolicy.FeesIncluded ); var prepareResponse = await sdk.PrepareLnurlPay(request: request); // If the fees are acceptable, continue to create the LNURL Pay var feeSats = prepareResponse.feeSats; Console.WriteLine($"Fees: {feeSats} sats"); // The receiver gets amountSats - feeSats ``` #### Javascript (Wasm) ```typescript // By default ({ type: 'feesExcluded' }), fees are added on top of the amount. // Use { type: 'feesIncluded' } to deduct fees from the amount instead. // The receiver gets amount minus fees. const optionalComment = '' const optionalValidateSuccessActionUrl = true const amountSats = BigInt(5_000) const feePolicy: FeePolicy = 'feesIncluded' const prepareResponse = await sdk.prepareLnurlPay({ amount: amountSats, payRequest, comment: optionalComment, validateSuccessActionUrl: optionalValidateSuccessActionUrl, tokenIdentifier: undefined, conversionOptions: undefined, feePolicy }) // If the fees are acceptable, continue to create the LNURL Pay const feeSats = prepareResponse.feeSats console.log(`Fees: ${feeSats} sats`) // The receiver gets amountSats - feeSats ``` #### React Native ```typescript // By default (FeePolicy.FeesExcluded), fees are added on top of the amount. // Use FeePolicy.FeesIncluded to deduct fees from the amount instead. // The receiver gets amount minus fees. const optionalComment = '' const optionalValidateSuccessActionUrl = true const amountSats = BigInt(5_000) const prepareResponse = await sdk.prepareLnurlPay({ amount: amountSats, payRequest, comment: optionalComment, validateSuccessActionUrl: optionalValidateSuccessActionUrl, tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: FeePolicy.FeesIncluded }) // If the fees are acceptable, continue to create the LNURL Pay const feeSats = prepareResponse.feeSats console.log(`Fees: ${feeSats} sats`) // The receiver gets amountSats - feeSats ``` #### Flutter ```dart // By default (FeePolicy.feesExcluded), fees are added on top of the amount. // Use FeePolicy.feesIncluded to deduct fees from the amount instead. // The receiver gets amount minus fees. String optionalComment = ""; bool optionalValidateSuccessActionUrl = true; BigInt amountSats = BigInt.from(5000); PrepareLnurlPayRequest request = PrepareLnurlPayRequest( amount: amountSats, payRequest: payRequest, comment: optionalComment, validateSuccessActionUrl: optionalValidateSuccessActionUrl, tokenIdentifier: null, conversionOptions: null, feePolicy: FeePolicy.feesIncluded, ); PrepareLnurlPayResponse prepareResponse = await sdk.prepareLnurlPay(request: request); // If the fees are acceptable, continue to create the LNURL Pay BigInt feeSats = prepareResponse.feeSats; print("Fees: $feeSats sats"); // The receiver gets amountSats - feeSats ``` #### Python ```python # By default (FeePolicy.FEES_EXCLUDED), fees are added on top of the amount. # Use FeePolicy.FEES_INCLUDED to deduct fees from the amount instead. # The receiver gets amount minus fees. amount_sats = 5_000 optional_comment = "" optional_validate_success_action_url = True request = PrepareLnurlPayRequest( amount=amount_sats, pay_request=pay_request, comment=optional_comment, validate_success_action_url=optional_validate_success_action_url, token_identifier=None, conversion_options=None, fee_policy=FeePolicy.FEES_INCLUDED, ) prepare_response = await sdk.prepare_lnurl_pay(request=request) # If the fees are acceptable, continue to create the LNURL Pay fee_sats = prepare_response.fee_sats logging.debug(f"Fees: {fee_sats} sats") # The receiver gets amount_sats - fee_sats ``` #### Go ```go // By default (FeePolicyFeesExcluded), fees are added on top of the amount. // Use FeePolicyFeesIncluded to deduct fees from the amount instead. // The receiver gets amount minus fees. amountSats := new(big.Int).SetInt64(5_000) optionalComment := "" optionalValidateSuccessActionUrl := true feePolicy := breez_sdk_spark.FeePolicyFeesIncluded request := breez_sdk_spark.PrepareLnurlPayRequest{ Amount: amountSats, PayRequest: payRequest, Comment: &optionalComment, ValidateSuccessActionUrl: &optionalValidateSuccessActionUrl, TokenIdentifier: nil, ConversionOptions: nil, FeePolicy: &feePolicy, } response, err := sdk.PrepareLnurlPay(request) if err != nil { return nil, err } // If the fees are acceptable, continue to create the LNURL Pay feeSats := response.FeeSats log.Printf("Fees: %v sats", feeSats) // The receiver gets amountSats - feeSats ``` ### Sending entire token balance When [stable balance](/guide/stable_balance.md) is active, you can send your entire wallet balance via LNURL. See [Sending entire balance](/guide/stable_balance.md#sending-entire-balance) for details. ## LNURL Payments API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.lnurl_pay Once the payment has been prepared and the fees are accepted, the payment can be sent by passing: - **Prepare Response** - The response from the [Preparing LNURL Payments](/guide/lnurl_pay.md#preparing-lnurl-payments) step. - **Idempotency Key** - An optional UUID that identifies the payment. If set, providing the same idempotency key for multiple requests will ensure that only one payment is made. ### Rust ```rust let optional_idempotency_key = Some("".to_string()); let response = sdk .lnurl_pay(LnurlPayRequest { prepare_response, idempotency_key: optional_idempotency_key, }) .await?; ``` ### Swift ```swift let optionalIdempotencyKey = "" let response = try await sdk.lnurlPay( request: LnurlPayRequest( prepareResponse: prepareResponse, idempotencyKey: optionalIdempotencyKey )) ``` ### Kotlin ```kotlin try { val optionalIdempotencyKey = "" val response = sdk.lnurlPay(LnurlPayRequest(prepareResponse, optionalIdempotencyKey)) } catch (e: Exception) { // handle error } ``` ### C# ```csharp var optionalIdempotencyKey = ""; var response = await sdk.LnurlPay( new LnurlPayRequest( prepareResponse: prepareResponse, idempotencyKey: optionalIdempotencyKey ) ); ``` ### Javascript (Wasm) ```typescript const optionalIdempotencyKey = '' const response = await sdk.lnurlPay({ prepareResponse, idempotencyKey: optionalIdempotencyKey }) ``` ### React Native ```typescript const optionalIdempotencyKey = '' const response = await sdk.lnurlPay({ prepareResponse, idempotencyKey: optionalIdempotencyKey }) ``` ### Flutter ```dart String? optionalIdempotencyKey = ""; LnurlPayResponse response = await sdk.lnurlPay( request: LnurlPayRequest( prepareResponse: prepareResponse, idempotencyKey: optionalIdempotencyKey), ); ``` ### Python ```python try: optional_idempotency_key = "" response = await sdk.lnurl_pay( LnurlPayRequest( prepare_response=prepare_response, idempotency_key=optional_idempotency_key, ) ) except Exception as error: logging.error(error) raise ``` ### Go ```go optionalIdempotencyKey := "" request := breez_sdk_spark.LnurlPayRequest{ PrepareResponse: prepareResponse, IdempotencyKey: &optionalIdempotencyKey, } response, err := sdk.LnurlPay(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } payment := response.Payment ``` **Developer note** By default when the LNURL-pay results in a success action with a URL, the URL is validated to check if there is a mismatch with the LNURL callback domain. You can disable this behaviour by setting the optional validation PrepareLnurlPayRequest param to false. ## Managing contacts You can save frequently used Lightning addresses as contacts for quick access. See [Managing contacts](/guide/contacts.md) for details. ## Supported Specs - [LUD-01](https://github.com/lnurl/luds/blob/luds/01.md) LNURL bech32 encoding - [LUD-06](https://github.com/lnurl/luds/blob/luds/06.md) `payRequest` spec - [LUD-09](https://github.com/lnurl/luds/blob/luds/09.md) `successAction` field for `payRequest` - [LUD-16](https://github.com/lnurl/luds/blob/luds/16.md) LN Address - [LUD-17](https://github.com/lnurl/luds/blob/luds/17.md) Support for lnurlp prefix with non-bech32-encoded LNURL URLs --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Managing contacts](/guide/contacts.md)** # Managing contacts Contacts allow you to save Lightning addresses for quick access. Each contact stores a name and a Lightning address, making it easy to send payments to frequently used recipients. Contacts are synced across all instances of the SDK. ## Adding a contact API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.add_contact To add a new contact, provide a name and a Lightning address. ### Rust ```rust let contact = sdk .add_contact(AddContactRequest { name: "Alice".to_string(), payment_identifier: "alice@example.com".to_string(), }) .await?; info!("Contact added: {:?}", contact); ``` ### Swift ```swift let contact = try await sdk.addContact( request: AddContactRequest( name: "Alice", paymentIdentifier: "alice@example.com" )) print("Contact added: \(contact)") ``` ### Kotlin ```kotlin val contact = sdk.addContact(AddContactRequest( name = "Alice", paymentIdentifier = "alice@example.com" )) // Log.v("Breez", "Contact added: $contact") ``` ### C# ```csharp var contact = await sdk.AddContact(request: new AddContactRequest( name: "Alice", paymentIdentifier: "alice@example.com" )); Console.WriteLine($"Contact added: {contact}"); ``` ### Javascript (Wasm) ```typescript const contact = await sdk.addContact({ name: 'Alice', paymentIdentifier: 'alice@example.com' }) console.log(`Contact added: ${JSON.stringify(contact)}`) ``` ### React Native ```typescript const contact = await sdk.addContact({ name: 'Alice', paymentIdentifier: 'alice@example.com' }) console.log(`Contact added: ${JSON.stringify(contact)}`) ``` ### Flutter ```dart AddContactRequest request = AddContactRequest( name: "Alice", paymentIdentifier: "alice@example.com", ); Contact contact = await sdk.addContact(request: request); print("Contact added: $contact"); ``` ### Python ```python contact = await sdk.add_contact( request=AddContactRequest( name="Alice", payment_identifier="alice@example.com", ) ) logging.debug(f"Contact added: {contact}") ``` ### Go ```go contact, err := sdk.AddContact(breez_sdk_spark.AddContactRequest{ Name: "Alice", PaymentIdentifier: "alice@example.com", }) if err != nil { return nil, err } log.Printf("Contact added: %v", contact) ``` ## Updating a contact API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.update_contact To update an existing contact, provide the contact ID along with the new name and Lightning address. ### Rust ```rust let contact_id = "contact-id".to_string(); let contact = sdk .update_contact(UpdateContactRequest { id: contact_id, name: "Alice Smith".to_string(), payment_identifier: "alice.smith@example.com".to_string(), }) .await?; info!("Contact updated: {:?}", contact); ``` ### Swift ```swift let contactId = "contact-id" let contact = try await sdk.updateContact( request: UpdateContactRequest( id: contactId, name: "Alice Smith", paymentIdentifier: "alice.smith@example.com" )) print("Contact updated: \(contact)") ``` ### Kotlin ```kotlin val contactId = "contact-id" val contact = sdk.updateContact(UpdateContactRequest( id = contactId, name = "Alice Smith", paymentIdentifier = "alice.smith@example.com" )) // Log.v("Breez", "Contact updated: $contact") ``` ### C# ```csharp var contactId = "contact-id"; var contact = await sdk.UpdateContact(request: new UpdateContactRequest( id: contactId, name: "Alice Smith", paymentIdentifier: "alice.smith@example.com" )); Console.WriteLine($"Contact updated: {contact}"); ``` ### Javascript (Wasm) ```typescript const contactId = 'contact-id' const contact = await sdk.updateContact({ id: contactId, name: 'Alice Smith', paymentIdentifier: 'alice.smith@example.com' }) console.log(`Contact updated: ${JSON.stringify(contact)}`) ``` ### React Native ```typescript const contactId = 'contact-id' const contact = await sdk.updateContact({ id: contactId, name: 'Alice Smith', paymentIdentifier: 'alice.smith@example.com' }) console.log(`Contact updated: ${JSON.stringify(contact)}`) ``` ### Flutter ```dart String contactId = "contact-id"; UpdateContactRequest request = UpdateContactRequest( id: contactId, name: "Alice Smith", paymentIdentifier: "alice.smith@example.com", ); Contact contact = await sdk.updateContact(request: request); print("Contact updated: $contact"); ``` ### Python ```python contact_id = "contact-id" contact = await sdk.update_contact( request=UpdateContactRequest( id=contact_id, name="Alice Smith", payment_identifier="alice.smith@example.com", ) ) logging.debug(f"Contact updated: {contact}") ``` ### Go ```go contactId := "contact-id" contact, err := sdk.UpdateContact(breez_sdk_spark.UpdateContactRequest{ Id: contactId, Name: "Alice Smith", PaymentIdentifier: "alice.smith@example.com", }) if err != nil { return nil, err } log.Printf("Contact updated: %v", contact) ``` ## Deleting a contact API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.delete_contact To remove a contact, pass its ID to the delete method. ### Rust ```rust let contact_id = "contact-id".to_string(); sdk.delete_contact(contact_id).await?; info!("Contact deleted"); ``` ### Swift ```swift let contactId = "contact-id" try await sdk.deleteContact(id: contactId) print("Contact deleted") ``` ### Kotlin ```kotlin val contactId = "contact-id" sdk.deleteContact(contactId) // Log.v("Breez", "Contact deleted") ``` ### C# ```csharp var contactId = "contact-id"; await sdk.DeleteContact(id: contactId); Console.WriteLine("Contact deleted"); ``` ### Javascript (Wasm) ```typescript const contactId = 'contact-id' await sdk.deleteContact(contactId) console.log('Contact deleted') ``` ### React Native ```typescript const contactId = 'contact-id' await sdk.deleteContact(contactId) console.log('Contact deleted') ``` ### Flutter ```dart String contactId = "contact-id"; await sdk.deleteContact(id: contactId); print("Contact deleted"); ``` ### Python ```python contact_id = "contact-id" await sdk.delete_contact(id=contact_id) logging.debug("Contact deleted") ``` ### Go ```go contactId := "contact-id" err := sdk.DeleteContact(contactId) if err != nil { return err } log.Printf("Contact deleted") ``` ## Listing contacts API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.list_contacts To retrieve your saved contacts, use the list method. The results support pagination through offset and limit parameters. ### Rust ```rust // List contacts with pagination (e.g., 10 contacts starting from offset 0) let contacts = sdk .list_contacts(ListContactsRequest { offset: Some(0), limit: Some(10), }) .await?; for contact in contacts { info!( "Contact: id={}, name={}, identifier={}", contact.id, contact.name, contact.payment_identifier ); } ``` ### Swift ```swift // List contacts with pagination (e.g., 10 contacts starting from offset 0) let contacts = try await sdk.listContacts( request: ListContactsRequest( offset: 0, limit: 10 )) for contact in contacts { print( "Contact: id=\(contact.id), name=\(contact.name), " + "identifier=\(contact.paymentIdentifier)") } ``` ### Kotlin ```kotlin // List contacts with pagination (e.g., 10 contacts starting from offset 0) val contacts = sdk.listContacts(ListContactsRequest( offset = 0u, limit = 10u )) for (contact in contacts) { // Log.v("Breez", "Contact: id=${contact.id}, name=${contact.name}, // identifier=${contact.paymentIdentifier}") } ``` ### C# ```csharp // List contacts with pagination (e.g., 10 contacts starting from offset 0) var contacts = await sdk.ListContacts(request: new ListContactsRequest( offset: 0, limit: 10 )); foreach (var contact in contacts) { Console.WriteLine($"Contact: id={contact.id}, name={contact.name}, " + $"identifier={contact.paymentIdentifier}"); } ``` ### Javascript (Wasm) ```typescript // List contacts with pagination (e.g., 10 contacts starting from offset 0) const contacts = await sdk.listContacts({ offset: 0, limit: 10 }) for (const contact of contacts) { console.log( `Contact: id=${contact.id}, name=${contact.name}, ` + `identifier=${contact.paymentIdentifier}` ) } ``` ### React Native ```typescript // List contacts with pagination (e.g., 10 contacts starting from offset 0) const contacts = await sdk.listContacts({ offset: 0, limit: 10 }) for (const contact of contacts) { console.log( `Contact: id=${contact.id}, name=${contact.name}, ` + `identifier=${contact.paymentIdentifier}` ) } ``` ### Flutter ```dart // List contacts with pagination (e.g., 10 contacts starting from offset 0) ListContactsRequest request = ListContactsRequest( offset: 0, limit: 10, ); List contacts = await sdk.listContacts(request: request); for (Contact contact in contacts) { print("Contact: id=${contact.id}, name=${contact.name}, identifier=${contact.paymentIdentifier}"); } ``` ### Python ```python # List contacts with pagination (e.g., 10 contacts starting from offset 0) contacts = await sdk.list_contacts( request=ListContactsRequest( offset=0, limit=10, ) ) for contact in contacts: logging.debug( f"Contact: id={contact.id}, name={contact.name}, " f"identifier={contact.payment_identifier}" ) ``` ### Go ```go // List contacts with pagination (e.g., 10 contacts starting from offset 0) offset := uint32(0) limit := uint32(10) contacts, err := sdk.ListContacts(breez_sdk_spark.ListContactsRequest{ Offset: &offset, Limit: &limit, }) if err != nil { return nil, err } for _, contact := range contacts { log.Printf( "Contact: id=%v, name=%v, identifier=%v", contact.Id, contact.Name, contact.PaymentIdentifier, ) } ``` **→ [Receiving payments using LNURL-Pay/Lightning address](/guide/receive_lnurl_pay.md)** # Receiving payments using LNURL-Pay and Lightning addresses ## What is a Lightning address? A Lightning address is a human-readable identifier formatted like an email address (e.g., `user@domain.com`) that can be used to receive Bitcoin payments over the Lightning Network. Behind the scenes, it uses the LNURL-Pay protocol to dynamically generate invoices when someone wants to send a payment to this address. ## Configuring a custom domain To use Lightning addresses with the Breez SDK, you first need to supply a domain. There are two options: 1. **Use a hosted LNURL server**: You can have your custom domain configured to an LNURL server run by Breez. 2. **Self-hosted LNURL server**: You can run your own [LNURL server](https://github.com/breez/spark-sdk/tree/main/crates/breez-sdk/lnurl) in a self-hosted environment. In case you choose to point your domain to a hosted LNURL server, you will need to add a CNAME record in your domain's DNS settings. > **Note:**: If you're using Cloudflare, make sure the CNAME record is set to 'DNS only' (not 'Proxied'). **Option 1: Using your domain without any subdomain** This points yourdomain.com directly to the LNURL server. Some DNS providers do not support this method. If yours doesn't support CNAME or ALIAS records for the root domain, you will need to configure your domain at the registrar level to use an external DNS provider (like Google Cloud DNS). * **Host/Name**: @ * **Type**: CNAME (or ALIAS if available) * **Value/Target**: breez.tips **Option 2: Using a subdomain** This points a subdomain like pay.yourdomain.com to the LNURL server. * **Host/Name**: pay (or your chosen prefix like payment, tip, donate) * **Type**: CNAME * **Value/Target**: breez.tips [Send us](mailto:contact@breez.technology) your domain name (e.g., yourdomain.com or pay.yourdomain.com), together with the Breez API key you want the LNURL payments on that domain to be associated with. We will verify and add it to our list of allowed domains. ## Configuring Lightning addresses for users Configure your domain in the SDK by passing the `lnurl_domain` parameter in the SDK configuration: ### Rust ```rust let mut config = default_config(Network::Mainnet); config.api_key = Some("your-api-key".to_string()); config.lnurl_domain = Some("yourdomain.com".to_string()); ``` ### Swift ```swift var config = defaultConfig(network: Network.mainnet) config.apiKey = "your-api-key" config.lnurlDomain = "yourdomain.com" ``` ### Kotlin ```kotlin val config = defaultConfig(Network.MAINNET) config.apiKey = "your-api-key" config.lnurlDomain = "yourdomain.com" ``` ### C# ```csharp var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { apiKey = "your-api-key", lnurlDomain = "yourdomain.com" }; ``` ### Javascript (Wasm) ```typescript const config = defaultConfig('mainnet') config.apiKey = 'your-api-key' config.lnurlDomain = 'yourdomain.com' ``` ### React Native ```typescript const config = defaultConfig(Network.Mainnet) config.apiKey = 'your-api-key' config.lnurlDomain = 'yourdomain.com' ``` ### Flutter ```dart final config = defaultConfig(network: Network.mainnet) .copyWith( apiKey: 'your-api-key', lnurlDomain: 'yourdomain.com' ); ``` ### Python ```python config = default_config(network=Network.MAINNET) config.api_key = "your-api-key" config.lnurl_domain = "yourdomain.com" ``` ### Go ```go lnurlDomain := "yourdomain.com" apiKey := "your-api-key" config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) config.ApiKey = &apiKey config.LnurlDomain = &lnurlDomain ``` ## Managing Lightning addresses API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.check_lightning_address_available The SDK provides several functions to manage Lightning addresses: ### Checking address availability Before registering a Lightning address, you can check if the username is available. In your UI you can use a quick check mark to show the address is available before registering. > **Note:** The answer is specific to this wallet. A username this wallet gave up earlier is reported as available to it, since only it can take that username back, while every other wallet is told the same username is unavailable. > **Note:** Each check is signed with the wallet's identity key, so it costs a signing operation and a server round trip. Where signing is remote or needs user approval that is slow, so check once the user finishes typing rather than on every keystroke. #### Rust ```rust let request = CheckLightningAddressRequest { username }; let is_available = sdk.check_lightning_address_available(request).await?; ``` #### Swift ```swift let request = CheckLightningAddressRequest( username: username ) let available = try await sdk.checkLightningAddressAvailable(req: request) ``` #### Kotlin ```kotlin val request = CheckLightningAddressRequest( username = username ) val available = sdk.checkLightningAddressAvailable(request) ``` #### C# ```csharp var request = new CheckLightningAddressRequest(username: username); var isAvailable = await sdk.CheckLightningAddressAvailable(request); ``` #### Javascript (Wasm) ```typescript const request = { username } const available = await sdk.checkLightningAddressAvailable(request) ``` #### React Native ```typescript const request = { username } const available = await sdk.checkLightningAddressAvailable(request) ``` #### Flutter ```dart final request = CheckLightningAddressRequest( username: username, ); final available = await sdk.checkLightningAddressAvailable(request: request); ``` #### Python ```python request = CheckLightningAddressRequest(username=username) is_available = await sdk.check_lightning_address_available(request) ``` #### Go ```go request := breez_sdk_spark.CheckLightningAddressRequest{ Username: username, } isAvailable, err := sdk.CheckLightningAddressAvailable(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return false, err } ``` ### Registering a Lightning address Once you've confirmed a username is available, you can register it by passing a username and a description. The username will be used in `username@domain.com`. The description will be included in lnurl metadata and as the invoice description, so this is what the sender will see. The description is optional, and will default to `Pay to username@domain.com`. > **Note:** Each user can have only one Lightning address per domain when using the Breez LNURL server. Registering a new address on the same domain will replace the previous one, but it won't be available to others. #### Rust ```rust let request = RegisterLightningAddressRequest { username, description, }; let address_info = sdk.register_lightning_address(request).await?; let lightning_address = address_info.lightning_address; let lnurl_url = address_info.lnurl.url; let lnurl_bech32 = address_info.lnurl.bech32; ``` #### Swift ```swift let request = RegisterLightningAddressRequest( username: username, description: description ) let addressInfo = try await sdk.registerLightningAddress(request: request) let lightningAddress = addressInfo.lightningAddress let lnurlUrl = addressInfo.lnurl.url let lnurlBech32 = addressInfo.lnurl.bech32 ``` #### Kotlin ```kotlin val request = RegisterLightningAddressRequest( username = username, description = description ) val addressInfo = sdk.registerLightningAddress(request) val lightningAddress = addressInfo.lightningAddress val lnurlUrl = addressInfo.lnurl.url val lnurlBech32 = addressInfo.lnurl.bech32 ``` #### C# ```csharp var request = new RegisterLightningAddressRequest( username: username, description: description ); var addressInfo = await sdk.RegisterLightningAddress(request); var lightningAddress = addressInfo.lightningAddress; var lnurlUrl = addressInfo.lnurl.url; var lnurlBech32 = addressInfo.lnurl.bech32; ``` #### Javascript (Wasm) ```typescript const request = { username, description } const addressInfo = await sdk.registerLightningAddress(request) const lightningAddress = addressInfo.lightningAddress const lnurlUrl = addressInfo.lnurl.url const lnurlBech32 = addressInfo.lnurl.bech32 ``` #### React Native ```typescript const request = { username, description } const addressInfo = await sdk.registerLightningAddress(request) const lightningAddress = addressInfo.lightningAddress const lnurlUrl = addressInfo.lnurl.url const lnurlBech32 = addressInfo.lnurl.bech32 ``` #### Flutter ```dart final request = RegisterLightningAddressRequest( username: username, description: description, ); final addressInfo = await sdk.registerLightningAddress(request: request); final lightningAddress = addressInfo.lightningAddress; final lnurlUrl = addressInfo.lnurl.url; final lnurlBech32 = addressInfo.lnurl.bech32; ``` #### Python ```python request = RegisterLightningAddressRequest( username=username, description=description ) address_info = await sdk.register_lightning_address(request) lightning_address = address_info.lightning_address lnurl_url = address_info.lnurl.url lnurl_bech32 = address_info.lnurl.bech32 ``` #### Go ```go request := breez_sdk_spark.RegisterLightningAddressRequest{ Username: username, Description: &description, } addressInfo, err := sdk.RegisterLightningAddress(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } _ = addressInfo.LightningAddress _ = addressInfo.Lnurl.Url _ = addressInfo.Lnurl.Bech32 ``` ### Retrieving Lightning address information You can retrieve information about the currently registered Lightning address. #### Rust ```rust let address_info_opt = sdk.get_lightning_address().await?; if let Some(info) = address_info_opt { let lightning_address = &info.lightning_address; let username = &info.username; let description = &info.description; let lnurl_url = &info.lnurl.url; let lnurl_bech32 = &info.lnurl.bech32; } ``` #### Swift ```swift if let addressInfo = try await sdk.getLightningAddress() { let lightningAddress = addressInfo.lightningAddress let username = addressInfo.username let description = addressInfo.description let lnurlUrl = addressInfo.lnurl.url let lnurlBech32 = addressInfo.lnurl.bech32 } ``` #### Kotlin ```kotlin val addressInfoOpt = sdk.getLightningAddress() if (addressInfoOpt != null) { val lightningAddress = addressInfoOpt.lightningAddress val username = addressInfoOpt.username val description = addressInfoOpt.description val lnurlUrl = addressInfoOpt.lnurl.url val lnurlBech32 = addressInfoOpt.lnurl.bech32 } ``` #### C# ```csharp var addressInfoOpt = await sdk.GetLightningAddress(); if (addressInfoOpt != null) { var lightningAddress = addressInfoOpt.lightningAddress; var username = addressInfoOpt.username; var description = addressInfoOpt.description; var lnurlUrl = addressInfoOpt.lnurl.url; var lnurlBech32 = addressInfoOpt.lnurl.bech32; } ``` #### Javascript (Wasm) ```typescript const addressInfoOpt = await sdk.getLightningAddress() if (addressInfoOpt != null) { const lightningAddress = addressInfoOpt.lightningAddress const username = addressInfoOpt.username const description = addressInfoOpt.description const lnurlUrl = addressInfoOpt.lnurl.url const lnurlBech32 = addressInfoOpt.lnurl.bech32 } ``` #### React Native ```typescript const addressInfoOpt = await sdk.getLightningAddress() if (addressInfoOpt != null) { const lightningAddress = addressInfoOpt.lightningAddress const username = addressInfoOpt.username const description = addressInfoOpt.description const lnurlUrl = addressInfoOpt.lnurl.url const lnurlBech32 = addressInfoOpt.lnurl.bech32 } ``` #### Flutter ```dart final addressInfoOpt = await sdk.getLightningAddress(); if (addressInfoOpt == null) { throw Exception("No Lightning Address registered for this user."); } final lightningAddress = addressInfoOpt.lightningAddress; final username = addressInfoOpt.username; final description = addressInfoOpt.description; final lnurlUrl = addressInfoOpt.lnurl.url; final lnurlBech32 = addressInfoOpt.lnurl.bech32; ``` #### Python ```python address_info_opt = await sdk.get_lightning_address() if address_info_opt is not None: lightning_address = address_info_opt.lightning_address username = address_info_opt.username description = address_info_opt.description lnurl_url = address_info_opt.lnurl.url lnurl_bech32 = address_info_opt.lnurl.bech32 ``` #### Go ```go addressInfoOpt, err := sdk.GetLightningAddress() if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } if addressInfoOpt != nil { _ = addressInfoOpt.LightningAddress _ = addressInfoOpt.Username _ = addressInfoOpt.Description _ = addressInfoOpt.Lnurl.Url _ = addressInfoOpt.Lnurl.Bech32 } ``` ### Transferring a Lightning address A user who already owns a registered Lightning address can hand it over to a different owner (pubkey) in a single atomic server operation: ownership is removed from the old pubkey and the new pubkey takes it in one step, without exposing a window during which the username could be snatched by a third party. > **Note:** Existing payments are not transferred to the new owner. Only the address. The flow has two steps, one method each, run by the current owner and then the new owner: **Step 1: Current owner (pubkey A)** calls `authorize_lightning_address_transfer` with the new owner's `identity_pubkey` (which the new owner obtains via `get_info`). It returns a `TransferAuthorization` (carrying the `username`, A's `pubkey`, `signature`, the `domain` the address is registered on, and the `timestamp` it was produced at), which grants B the right to take over the username. > **Note:** The two owners sign different messages: A's names A as the outgoing owner and B as the incoming one, and B's names the description B is choosing, so neither signature can stand in for the other. Both cover the same `domain` and `timestamp`, so an authorization is only valid on the server it was made for, and only for 10 minutes. Only B can submit the transfer, because the server requires B's own signature alongside A's; A's authorization alone doesn't let any third party move the username. #### Rust ```rust let authorization = current_owner_sdk .authorize_lightning_address_transfer(AuthorizeTransferRequest { transferee_pubkey: transferee_pubkey.to_string(), }) .await?; ``` #### Swift ```swift let authorization = try await currentOwnerSdk.authorizeLightningAddressTransfer( request: AuthorizeTransferRequest( transfereePubkey: transfereePubkey ) ) ``` #### Kotlin ```kotlin val authorization = currentOwnerSdk.authorizeLightningAddressTransfer( AuthorizeTransferRequest( transfereePubkey = transfereePubkey, ) ) ``` #### C# ```csharp var authorization = await currentOwnerSdk.AuthorizeLightningAddressTransfer( new AuthorizeTransferRequest( transfereePubkey: transfereePubkey)); ``` #### Javascript (Wasm) ```typescript const authorization = await currentOwnerSdk.authorizeLightningAddressTransfer({ transfereePubkey }) ``` #### React Native ```typescript const authorization = await currentOwnerSdk.authorizeLightningAddressTransfer({ transfereePubkey }) ``` #### Flutter ```dart final authorization = await currentOwnerSdk.authorizeLightningAddressTransfer( request: AuthorizeTransferRequest( transfereePubkey: transfereePubkey, ), ); ``` #### Python ```python request = AuthorizeTransferRequest( transferee_pubkey=transferee_pubkey ) authorization = await current_owner_sdk.authorize_lightning_address_transfer(request) ``` #### Go ```go request := breez_sdk_spark.AuthorizeTransferRequest{ TransfereePubkey: transfereePubkey, } authorization, err := currentOwnerSdk.AuthorizeLightningAddressTransfer(request) if err != nil { return nil, err } ``` The returned `TransferAuthorization` is then handed to the new owner over any channel. In an in-app migration, where a user moves their username from an old wallet to a new one, the app holds both SDK instances and passes it directly between them; to hand the username to a separate wallet, share it as a QR code or link. It carries everything B needs to claim. Because the authorization expires 10 minutes after it is produced, generate it when B is ready to claim rather than ahead of time. If B claims too late, the call fails and A simply authorizes again. > **Note:** Both wallets must be on an SDK version that signs the timestamped messages described above. A transfer between an older wallet and a newer one is rejected as an invalid signature. **Step 2: New owner (pubkey B)** calls `claim_lightning_address_transfer`, passing A's authorization. The SDK submits the transfer to the server which, in one transaction, verifies B's request signature, verifies A's authorization, and transfers ownership, returning the newly-owned `LightningAddressInfo`. #### Rust ```rust let address = new_owner_sdk .claim_lightning_address_transfer(ClaimTransferRequest { authorization, description, }) .await?; let lightning_address = address.lightning_address; let lnurl_url = address.lnurl.url; let lnurl_bech32 = address.lnurl.bech32; ``` #### Swift ```swift let addressInfo = try await newOwnerSdk.claimLightningAddressTransfer( request: ClaimTransferRequest( authorization: authorization, description: description ) ) let lightningAddress = addressInfo.lightningAddress let lnurlUrl = addressInfo.lnurl.url let lnurlBech32 = addressInfo.lnurl.bech32 ``` #### Kotlin ```kotlin val address = newOwnerSdk.claimLightningAddressTransfer( ClaimTransferRequest( authorization = authorization, description = description, ) ) val lightningAddress = address.lightningAddress val lnurlUrl = address.lnurl.url val lnurlBech32 = address.lnurl.bech32 ``` #### C# ```csharp var address = await newOwnerSdk.ClaimLightningAddressTransfer( new ClaimTransferRequest( authorization: authorization, description: description )); var lightningAddress = address.lightningAddress; var lnurlUrl = address.lnurl.url; var lnurlBech32 = address.lnurl.bech32; ``` #### Javascript (Wasm) ```typescript const addressInfo = await newOwnerSdk.claimLightningAddressTransfer({ authorization, description }) const lightningAddress = addressInfo.lightningAddress const lnurlUrl = addressInfo.lnurl.url const lnurlBech32 = addressInfo.lnurl.bech32 ``` #### React Native ```typescript const addressInfo = await newOwnerSdk.claimLightningAddressTransfer({ authorization, description }) const lightningAddress = addressInfo.lightningAddress const lnurlUrl = addressInfo.lnurl.url const lnurlBech32 = addressInfo.lnurl.bech32 ``` #### Flutter ```dart final address = await newOwnerSdk.claimLightningAddressTransfer( request: ClaimTransferRequest( authorization: authorization, description: description, ), ); final lightningAddress = address.lightningAddress; final lnurlUrl = address.lnurl.url; final lnurlBech32 = address.lnurl.bech32; ``` #### Python ```python request = ClaimTransferRequest( authorization=authorization, description=description ) address_info = await new_owner_sdk.claim_lightning_address_transfer(request) lightning_address = address_info.lightning_address lnurl_url = address_info.lnurl.url lnurl_bech32 = address_info.lnurl.bech32 ``` #### Go ```go request := breez_sdk_spark.ClaimTransferRequest{ Authorization: authorization, Description: &description, } address, err := newOwnerSdk.ClaimLightningAddressTransfer(request) if err != nil { return nil, err } _ = address.LightningAddress _ = address.Lnurl.Url _ = address.Lnurl.Bech32 ``` If pubkey B had a different username registered, it is replaced by the transferred one and stays reserved for B. The server rejects the call if pubkey A does not currently own the username (e.g. the name was already transferred to a third pubkey). ### Deleting a Lightning address When a user no longer wants to use the Lightning address, you can delete it. > **Note:** The username stays reserved for this wallet after deletion. While the reservation stands no one else can register it, so senders who saved the old address are not redirected to a stranger, and the wallet can register it again later. How long a reservation stands is up to the server. #### Rust ```rust sdk.delete_lightning_address().await?; ``` #### Swift ```swift try await sdk.deleteLightningAddress() ``` #### Kotlin ```kotlin sdk.deleteLightningAddress() ``` #### C# ```csharp await sdk.DeleteLightningAddress(); ``` #### Javascript (Wasm) ```typescript await sdk.deleteLightningAddress() ``` #### React Native ```typescript await sdk.deleteLightningAddress() ``` #### Flutter ```dart await sdk.deleteLightningAddress(); ``` #### Python ```python await sdk.delete_lightning_address() ``` #### Go ```go err := sdk.DeleteLightningAddress() if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } ``` ### Listening for Lightning address changes When using the SDK on multiple devices, Lightning address changes made on one device are automatically synced to others. The SDK emits a `SdkEvent::LightningAddressChanged` event when a change from another device is detected, containing the updated `LightningAddressInfo` or no value if the address was deleted. See [Listening to events](/guide/events.md) for how to subscribe to events. ## Accessing LNURL payment metadata When receiving payments via LNURL-Pay or Lightning addresses, additional metadata may be included with the payment. This metadata is available on the received payment. ### Sender comment If the sender includes a comment with their payment (as defined in [LUD-12](https://github.com/lnurl/luds/blob/luds/12.md)), it will be available on the received payment. This is the message that the sender wrote when making the payment. #### Rust ```rust // Check if this is a lightning payment with LNURL receive metadata if let Some(PaymentDetails::Lightning { lnurl_receive_metadata: Some(metadata), .. }) = payment.details { // Access the sender comment if present if let Some(comment) = metadata.sender_comment { println!("Sender comment: {}", comment); } } ``` #### Swift ```swift // Check if this is a lightning payment with LNURL receive metadata if case .lightning(let details) = payment.details { // Access the sender comment if present if let metadata = details.lnurlReceiveMetadata, let comment = metadata.senderComment { print("Sender comment: \(comment)") } } ``` #### Kotlin ```kotlin // Check if this is a lightning payment with LNURL receive metadata if (payment.details is PaymentDetails.Lightning) { val details = payment.details as PaymentDetails.Lightning val metadata = details.lnurlReceiveMetadata // Access the sender comment if present metadata?.senderComment?.let { comment -> println("Sender comment: $comment") } } ``` #### C# ```csharp // Check if this is a lightning payment with LNURL receive metadata if (payment.details is PaymentDetails.Lightning lightningDetails) { var metadata = lightningDetails.lnurlReceiveMetadata; // Access the sender comment if present if (metadata?.senderComment != null) { Console.WriteLine($"Sender comment: {metadata.senderComment}"); } } ``` #### Javascript (Wasm) ```typescript // Check if this is a lightning payment with LNURL receive metadata if (payment.details?.type === 'lightning') { const metadata = payment.details.lnurlReceiveMetadata // Access the sender comment if present if (metadata?.senderComment != null) { console.log('Sender comment:', metadata.senderComment) } } ``` #### React Native ```typescript // Check if this is a lightning payment with LNURL receive metadata if (payment.details?.tag === PaymentDetails_Tags.Lightning) { const metadata = payment.details.inner.lnurlReceiveMetadata // Access the sender comment if present if (metadata?.senderComment != null) { console.log('Sender comment:', metadata.senderComment) } } ``` #### Flutter ```dart // Check if this is a lightning payment with LNURL receive metadata if (payment.details case PaymentDetails_Lightning lightningDetails) { final metadata = lightningDetails.lnurlReceiveMetadata; // Access the sender comment if present final comment = metadata?.senderComment; if (comment != null) { print('Sender comment: $comment'); } } ``` #### Python ```python # Check if this is a lightning payment with LNURL receive metadata if isinstance(payment.details, PaymentDetails.LIGHTNING): metadata = payment.details.lnurl_receive_metadata # Access the sender comment if present if metadata is not None and metadata.sender_comment is not None: print(f"Sender comment: {metadata.sender_comment}") ``` #### Go ```go // Check if this is a lightning payment with LNURL receive metadata if lightningDetails, ok := (*payment.Details).(breez_sdk_spark.PaymentDetailsLightning); ok { metadata := lightningDetails.LnurlReceiveMetadata // Access the sender comment if present if metadata != nil && metadata.SenderComment != nil { println("Sender comment:", *metadata.SenderComment) } } ``` ### Nostr Zap request If the payment was sent as a Nostr Zap (as defined in [NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md)), the received payment will include the zap request event. It carries the signed Nostr event (kind 9734) used to create the zap, and will also include the zap receipt event (kind 9735) once that has been created and published. #### Rust ```rust // Check if this is a lightning payment with LNURL receive metadata if let Some(PaymentDetails::Lightning { lnurl_receive_metadata: Some(metadata), .. }) = payment.details { // Access the Nostr zap request if present if let Some(zap_request) = metadata.nostr_zap_request { // The zap_request is a JSON string containing the Nostr event (kind 9734) println!("Nostr zap request: {}", zap_request); } // Access the Nostr zap receipt if present if let Some(zap_receipt) = metadata.nostr_zap_receipt { // The zap_receipt is a JSON string containing the Nostr event (kind 9735) println!("Nostr zap receipt: {}", zap_receipt); } } ``` #### Swift ```swift // Check if this is a lightning payment with LNURL receive metadata if case .lightning(let details) = payment.details { if let metadata = details.lnurlReceiveMetadata { // Access the Nostr zap request if present if let zapRequest = metadata.nostrZapRequest { // The zapRequest is a JSON string containing the Nostr event (kind 9734) print("Nostr zap request: \(zapRequest)") } // Access the Nostr zap receipt if present if let zapReceipt = metadata.nostrZapReceipt { // The zapReceipt is a JSON string containing the Nostr event (kind 9735) print("Nostr zap receipt: \(zapReceipt)") } } } ``` #### Kotlin ```kotlin // Check if this is a lightning payment with LNURL receive metadata if (payment.details is PaymentDetails.Lightning) { val details = payment.details as PaymentDetails.Lightning val metadata = details.lnurlReceiveMetadata // Access the Nostr zap request if present metadata?.nostrZapRequest?.let { zapRequest -> // The zapRequest is a JSON string containing the Nostr event (kind 9734) println("Nostr zap request: $zapRequest") } // Access the Nostr zap receipt if present metadata?.nostrZapReceipt?.let { zapReceipt -> // The zapReceipt is a JSON string containing the Nostr event (kind 9735) println("Nostr zap receipt: $zapReceipt") } } ``` #### C# ```csharp // Check if this is a lightning payment with LNURL receive metadata if (payment.details is PaymentDetails.Lightning lightningDetails) { var metadata = lightningDetails.lnurlReceiveMetadata; if (metadata != null) { // Access the Nostr zap request if present if (metadata.nostrZapRequest != null) { // The nostrZapRequest is a JSON string containing the Nostr event (kind 9734) Console.WriteLine($"Nostr zap request: {metadata.nostrZapRequest}"); } // Access the Nostr zap receipt if present if (metadata.nostrZapReceipt != null) { // The nostrZapReceipt is a JSON string containing the Nostr event (kind 9735) Console.WriteLine($"Nostr zap receipt: {metadata.nostrZapReceipt}"); } } } ``` #### Javascript (Wasm) ```typescript // Check if this is a lightning payment with LNURL receive metadata if (payment.details?.type === 'lightning') { const metadata = payment.details.lnurlReceiveMetadata // Access the Nostr zap request if present if (metadata?.nostrZapRequest != null) { // The nostrZapRequest is a JSON string containing the Nostr event (kind 9734) console.log('Nostr zap request:', metadata.nostrZapRequest) } // Access the Nostr zap receipt if present if (metadata?.nostrZapReceipt != null) { // The nostrZapReceipt is a JSON string containing the Nostr event (kind 9735) console.log('Nostr zap receipt:', metadata.nostrZapReceipt) } } ``` #### React Native ```typescript // Check if this is a lightning payment with LNURL receive metadata if (payment.details?.tag === PaymentDetails_Tags.Lightning) { const metadata = payment.details.inner.lnurlReceiveMetadata // Access the Nostr zap request if present if (metadata?.nostrZapRequest != null) { // The nostrZapRequest is a JSON string containing the Nostr event (kind 9734) console.log('Nostr zap request:', metadata.nostrZapRequest) } // Access the Nostr zap receipt if present if (metadata?.nostrZapReceipt != null) { // The nostrZapReceipt is a JSON string containing the Nostr event (kind 9735) console.log('Nostr zap receipt:', metadata.nostrZapReceipt) } } ``` #### Flutter ```dart // Check if this is a lightning payment with LNURL receive metadata if (payment.details case PaymentDetails_Lightning lightningDetails) { final metadata = lightningDetails.lnurlReceiveMetadata; if (metadata != null) { // Access the Nostr zap request if present final zapRequest = metadata.nostrZapRequest; if (zapRequest != null) { // The zapRequest is a JSON string containing the Nostr event (kind 9734) print('Nostr zap request: $zapRequest'); } // Access the Nostr zap receipt if present final zapReceipt = metadata.nostrZapReceipt; if (zapReceipt != null) { // The zapReceipt is a JSON string containing the Nostr event (kind 9735) print('Nostr zap receipt: $zapReceipt'); } } } ``` #### Python ```python # Check if this is a lightning payment with LNURL receive metadata if isinstance(payment.details, PaymentDetails.LIGHTNING): metadata = payment.details.lnurl_receive_metadata if metadata is not None: # Access the Nostr zap request if present if metadata.nostr_zap_request is not None: # The nostr_zap_request is a JSON string containing the Nostr event (kind 9734) print(f"Nostr zap request: {metadata.nostr_zap_request}") # Access the Nostr zap receipt if present if metadata.nostr_zap_receipt is not None: # The nostr_zap_receipt is a JSON string containing the Nostr event (kind 9735) print(f"Nostr zap receipt: {metadata.nostr_zap_receipt}") ``` #### Go ```go // Check if this is a lightning payment with LNURL receive metadata if lightningDetails, ok := (*payment.Details).(breez_sdk_spark.PaymentDetailsLightning); ok { metadata := lightningDetails.LnurlReceiveMetadata if metadata != nil { // Access the Nostr zap request if present if metadata.NostrZapRequest != nil { // The NostrZapRequest is a JSON string containing the Nostr event (kind 9734) println("Nostr zap request:", *metadata.NostrZapRequest) } // Access the Nostr zap receipt if present if metadata.NostrZapReceipt != nil { // The NostrZapReceipt is a JSON string containing the Nostr event (kind 9735) println("Nostr zap receipt:", *metadata.NostrZapReceipt) } } } ``` ### Payment verification (LUD-21) Payments received through your Lightning address support [LUD-21](https://github.com/lnurl/luds/blob/luds/21.md) invoice verification, allowing third parties to verify payment completion via a public verify URL. ## Payment notifications You can receive webhook notifications when your users get paid via their Lightning Address. See [Lightning Address payment notifications](/guide/lnurl_webhooks.md) for details. --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Lightning Address payment notifications](/guide/lnurl_webhooks.md)** # Lightning Address payment notifications When one of your users receives a payment to their Lightning Address, Breez can send a webhook to your server. Payments are received automatically without any user interaction — the webhook simply lets you know it happened. For example, you could send the user a push notification, update a balance in your backend, trigger a fulfillment flow, or log the event for analytics. ## How it works Your users' Lightning Addresses are served by the Breez LNURL server. When a payment comes in, the LNURL server sends a webhook to your server. ![Webhook flow](/guide/images/lnurl_webhook_flow.svg) As an example, if you want to send push notifications to your users, you could run a Notification Delivery Service (NDS) that receives the webhook and forwards a push notification to the user's device: ![NDS push notification flow](/guide/images/lnurl_webhook_nds.svg) ## Getting started To start receiving webhooks, [send us](mailto:contact@breez.technology) your webhook endpoint URL. Breez will configure it for your domain so that all Lightning Address payments on that domain trigger a POST request to your endpoint. Your endpoint should accept `POST` requests with a JSON body and respond with a `2xx` status code to acknowledge receipt. ## Signature verification Every webhook request includes an `X-Breez-Signature` header containing a hex-encoded HMAC-SHA256 signature of the raw request body. You should verify this signature to ensure the request came from Breez and was not tampered with. The signing secret is provided to you during webhook setup. To verify: 1. Compute the HMAC-SHA256 of the raw request body using your shared secret. 2. Hex-encode the result. 3. Compare it to the value in the `X-Breez-Signature` header. **Node.js example:** ```javascript const crypto = require('crypto'); function verifyWebhookSignature(secret, body, signatureHeader) { const expected = crypto .createHmac('sha256', secret) .update(body) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signatureHeader), ); } ``` **Python example:** ```python import hmac import hashlib def verify_webhook_signature(secret: str, body: bytes, signature_header: str) -> bool: expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, signature_header) ``` ## Payload All payloads use a `{ "template": "...", "data": { ... } }` envelope. Currently the only template is `spark_payment_received`: ```json { "template": "spark_payment_received", "data": { "payment_hash": "abc123...", "invoice": "lnbc50u1p...", "preimage": "def456...", "amount_sat": 50000, "user_pubkey": "02abc123...", "lightning_address": "alice@yourdomain.com", "sender_comment": "Thanks!", "nostr_zap_request": "{\"id\":\"...\", \"kind\":9734, ...}", "timestamp": 1711929600000 } } ``` | Field | Type | Description | |-------|------|-------------| | `payment_hash` | `string` | Hex-encoded payment hash | | `invoice` | `string` | BOLT11 invoice that was paid | | `preimage` | `string` | Hex-encoded payment preimage | | `amount_sat` | `number \| null` | Amount received in satoshis. May be `null` in rare cases where the amount is not available. | | `user_pubkey` | `string` | The Spark identity public key of the user who received the payment | | `lightning_address` | `string \| null` | The Lightning Address that received the payment (e.g. `alice@yourdomain.com`) | | `sender_comment` | `string \| null` | Comment attached by the sender, if any | | `nostr_zap_request` | `string \| null` | The raw Nostr Zap Request (Kind 9734) JSON string, if the payment was a Nostr Zap | | `timestamp` | `number` | Milliseconds since Unix epoch when the webhook was enqueued | ## Retries If your endpoint is unreachable or responds with a non-2xx status code, Breez will automatically retry delivery with exponential backoff. Because of this, your endpoint may receive the same webhook more than once for the same payment — use the `paymentHash` field to deduplicate. ## Best practices - **Verify the signature.** Always verify the `X-Breez-Signature` header before processing the webhook. Reject requests with missing or invalid signatures. - **Return 2xx quickly.** Do your processing asynchronously after acknowledging the webhook. Slow responses will be treated as failures and retried. - **Deduplicate on `paymentHash`.** The same payment may be delivered more than once due to retries. - **Use `lightningAddress` or `userPubkey` to identify the user.** These fields tell you which user received the payment. **→ [Receiving payments using LNURL-Withdraw](/guide/lnurl_withdraw.md)** # Receiving payments using LNURL-Withdraw API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.lnurl_withdraw After [parsing](/guide/parse.md) an LNURL-Withdraw input, you can use the resulting input data to initiate a withdrawal from an LNURL service. By default, this function returns immediately. You can override this behavior by specifying a completion timeout in seconds. If the completion timeout is hit, a pending payment object is returned if available. If the payment completes, the completed payment object is returned. **Developer note** The minimum and maximum withdrawable amount returned from calling parse is denominated in millisatoshi. ## Rust ```rust // Endpoint can also be of the form: // lnurlw://domain.com/lnurl-withdraw?key=val let lnurl_withdraw_url = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4exctthd96xserjv9mn7um9wdekjmmw843xxwpexdnxzen9vgunsvfexq6rvdecx93rgdmyxcuxverrvcursenpxvukzv3c8qunsdecx33nzwpnvg6ryc3hv93nzvecxgcxgwp3h33lxk"; if let Ok(InputType::LnurlWithdraw(withdraw_request)) = sdk.parse(lnurl_withdraw_url).await { // Amount to withdraw in sats between min/max withdrawable amounts let amount_sats = 5_000; let optional_completion_timeout_secs = Some(30); let response = sdk .lnurl_withdraw(LnurlWithdrawRequest { amount_sats, withdraw_request, completion_timeout_secs: optional_completion_timeout_secs, }) .await?; let payment = response.payment; info!("Payment: {payment:?}"); } ``` ## Swift ```swift // Endpoint can also be of the form: // lnurlw://domain.com/lnurl-withdraw?key=val let lnurlWithdrawUrl = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4exctthd96xserjv9mn7um9wdekjmmw843xxwpexdnxzen9vgunsvfexq6rvdecx93rgdmyxcuxverrvcursenpxvukzv3c8qunsdecx33nzwpnvg6ryc3hv93nzvecxgcxgwp3h33lxk" let inputType = try await sdk.parse(input: lnurlWithdrawUrl) if case .lnurlWithdraw(v1: let withdrawRequest) = inputType { // Amount to withdraw in sats between min/max withdrawable amounts let amountSats: UInt64 = 5_000 let optionalCompletionTimeoutSecs: UInt32 = 30 let request = LnurlWithdrawRequest( amountSats: amountSats, withdrawRequest: withdrawRequest, completionTimeoutSecs: optionalCompletionTimeoutSecs ) let response = try await sdk.lnurlWithdraw(request: request) let payment = response.payment print("Payment: \(payment)") } ``` ## Kotlin ```kotlin // Endpoint can also be of the form: // lnurlw://domain.com/lnurl-withdraw?key=val val lnurlWithdrawUrl = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7..." try { val inputType = sdk.parse(lnurlWithdrawUrl) if (inputType is InputType.LnurlWithdraw) { // Amount to withdraw in sats between min/max withdrawable amounts val amountSats = 5_000.toULong() val withdrawRequest = inputType.v1 val optionalCompletionTimeoutSecs = 30.toUInt() val request = LnurlWithdrawRequest( amountSats, withdrawRequest, optionalCompletionTimeoutSecs ) val response = sdk.lnurlWithdraw(request) val payment = response.payment // Log.v("Breez", "Payment: $payment") } } catch (e: Exception) { // handle error } ``` ## C# ```csharp // Endpoint can also be of the form: // lnurlw://domain.com/lnurl-withdraw?key=val var lnurlWithdrawUrl = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4exctthd96xserjv9mn7um9wdekj" + "mmw843xxwpexdnxzen9vgunsvfexq6rvdecx93rgdmyxcuxverrvcursenpxvukzv3c8" + "qunsdecx33nzwpnvg6ryc3hv93nzvecxgcxgwp3h33lxk"; var inputType = await sdk.Parse(lnurlWithdrawUrl); if (inputType is InputType.LnurlWithdraw lnurlWithdraw) { // Amount to withdraw in sats between min/max withdrawable amounts var amountSats = 5_000UL; var withdrawRequest = lnurlWithdraw.v1; var optionalCompletionTimeoutSecs = 30U; var request = new LnurlWithdrawRequest( amountSats: amountSats, withdrawRequest: withdrawRequest, completionTimeoutSecs: optionalCompletionTimeoutSecs ); var response = await sdk.LnurlWithdraw(request: request); var payment = response.payment; Console.WriteLine($"Payment: {payment}"); } ``` ## Javascript (Wasm) ```typescript // Endpoint can also be of the form: // lnurlw://domain.com/lnurl-withdraw?key=val const lnurlWithdrawUrl = 'lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4exctthd96xserjv9mn7um9wdekjmmw843xxwpexdnxzen9vgunsvfexq6rvdecx93rgdmyxcuxverrvcursenpxvukzv3c8qunsdecx33nzwpnvg6ryc3hv93nzvecxgcxgwp3h33lxk' const input = await sdk.parse(lnurlWithdrawUrl) if (input.type === 'lnurlWithdraw') { // Amount to withdraw in sats between min/max withdrawable amounts const amountSats = 5_000 const withdrawRequest = input const optionalCompletionTimeoutSecs = 30 const response = await sdk.lnurlWithdraw({ amountSats, withdrawRequest, completionTimeoutSecs: optionalCompletionTimeoutSecs }) const payment = response.payment console.log(`Payment: ${JSON.stringify(payment)}`) } ``` ## React Native ```typescript // Endpoint can also be of the form: // lnurlw://domain.com/lnurl-withdraw?key=val const lnurlWithdrawUrl = 'lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4exctthd96xserjv9mn7um9wdekjmmw843xxwpexdnxzen9vgunsvfexq6rvdecx93rgdmyxcuxverrvcursenpxvukzv3c8qunsdecx33nzwpnvg6ryc3hv93nzvecxgcxgwp3h33lxk' const input = await sdk.parse(lnurlWithdrawUrl) if (input.tag === InputType_Tags.LnurlWithdraw) { // Amount to withdraw in sats between min/max withdrawable amounts const amountSats = BigInt(5_000) const withdrawRequest = input.inner[0] const optionalCompletionTimeoutSecs = 30 const response = await sdk.lnurlWithdraw({ amountSats, withdrawRequest, completionTimeoutSecs: optionalCompletionTimeoutSecs }) const payment = response.payment console.log(`Payment: ${JSON.stringify(payment)}`) } ``` ## Flutter ```dart /// Endpoint can also be of the form: /// lnurlw://domain.com/lnurl-withdraw?key=val String lnurlWithdrawUrl = "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4exctthd96xserjv9mn7um9wdekjmmw843xxwpexdnxzen9vgunsvfexq6rvdecx93rgdmyxcuxverrvcursenpxvukzv3c8qunsdecx33nzwpnvg6ryc3hv93nzvecxgcxgwp3h33lxk"; InputType inputType = await sdk.parse(input: lnurlWithdrawUrl); if (inputType is InputType_LnurlWithdraw) { // Amount to withdraw in sats between min/max withdrawable amounts BigInt amountSats = BigInt.from(5000); LnurlWithdrawRequestDetails withdrawRequest = inputType.field0; int optionalCompletionTimeoutSecs = 30; LnurlWithdrawRequest request = LnurlWithdrawRequest( amountSats: amountSats, withdrawRequest: withdrawRequest, completionTimeoutSecs: optionalCompletionTimeoutSecs, ); LnurlWithdrawResponse response = await sdk.lnurlWithdraw(request: request); Payment? payment = response.payment; print('Payment: $payment'); } ``` ## Python ```python # Endpoint can also be of the form: # lnurlw://domain.com/lnurl-withdraw?key=val lnurl_withdraw_url = ( "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4exctthd96xserjv9mn7um9wdekj" "mmw843xxwpexdnxzen9vgunsvfexq6rvdecx93rgdmyxcuxverrvcursenpxvukzv3c8" "qunsdecx33nzwpnvg6ryc3hv93nzvecxgcxgwp3h33lxk" ) try: input_type = await sdk.parse(lnurl_withdraw_url) if isinstance(input_type, InputType.LNURL_WITHDRAW): # Amount to withdraw in sats between min/max withdrawable amounts amount_sats = 5_000 withdraw_request = input_type[0] optional_completion_timeout_secs = 30 request = LnurlWithdrawRequest( amount_sats=amount_sats, withdraw_request=withdraw_request, completion_timeout_secs=optional_completion_timeout_secs, ) response = await sdk.lnurl_withdraw(request=request) payment = response.payment logging.debug(f"Payment: {payment}") return response except Exception as error: logging.error(error) raise ``` ## Go ```go // Endpoint can also be of the form: // lnurlw://domain.com/lnurl-withdraw?key=val lnurlWithdrawUrl := "lnurl1dp68gurn8ghj7mr0vdskc6r0wd6z7mrww4exctthd96xserjv9mn7um9wdekj" + "mmw843xxwpexdnxzen9vgunsvfexq6rvdecx93rgdmyxcuxverrvcursenpxvukzv3c8qunsdecx33nzwpnvg6ryc3hv93nzvecxgcxgwp3h33lxk" input, err := sdk.Parse(lnurlWithdrawUrl) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } switch inputType := input.(type) { case breez_sdk_spark.InputTypeLnurlWithdraw: // Amount to withdraw in sats between min/max withdrawable amounts amountSats := uint64(5_000) withdrawRequest := inputType.Field0 optionalCompletionTimeoutSecs := uint32(30) request := breez_sdk_spark.LnurlWithdrawRequest{ AmountSats: amountSats, WithdrawRequest: withdrawRequest, CompletionTimeoutSecs: &optionalCompletionTimeoutSecs, } response, err := sdk.LnurlWithdraw(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } payment := response.Payment log.Printf("Payment: %#v", payment) return &response, nil } ``` ## Supported Specs - [LUD-01](https://github.com/lnurl/luds/blob/luds/01.md) LNURL bech32 encoding - [LUD-03](https://github.com/lnurl/luds/blob/luds/03.md) `withdrawRequest` spec - [LUD-17](https://github.com/lnurl/luds/blob/luds/17.md) Support for lnurlw prefix with non-bech32-encoded LNURL URLs **→ [Using LNURL-Auth](/guide/lnurl_auth.md)** # Using LNURL-Auth API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.lnurl_auth LNURL-Auth allows users to authenticate with services using their Lightning app, without requiring passwords or usernames. The Breez SDK supports LNURL-Auth following the LUD-04 and LUD-05 specifications. ## How it works LNURL-Auth uses cryptographic key derivation to generate domain-specific keys, ensuring that: - Each service gets a unique authentication key - Your master key remains private - Authentication is secure and passwordless The SDK handles: 1. Domain-specific key derivation (LUD-05) 2. Challenge signing 3. Callback to the LNURL service ## Parsing LNURL-Auth URLs After [parsing](/guide/parse.md) an LNURL-Auth URL, you'll receive an `LnurlAuthRequestDetails` object containing: - **k1** - The authentication challenge (hex-encoded 32 bytes) - **action** - Optional action type: `register`, `login`, `link`, or `auth` - **domain** - The service domain requesting authentication - **url** - The callback URL ### Rust ```rust // LNURL-auth URL from a service // Can be in the form: // - lnurl1... (bech32 encoded) // - https://service.com/lnurl-auth?tag=login&k1=... let lnurl_auth_url = "lnurl1..."; if let Ok(InputType::LnurlAuth(request_data)) = sdk.parse(lnurl_auth_url).await { info!("Domain: {}", request_data.domain); info!("Action: {:?}", request_data.action); // Show domain to user and ask for confirmation // This is important for security } ``` ### Swift ```swift // LNURL-auth URL from a service // Can be in the form: // - lnurl1... (bech32 encoded) // - https://service.com/lnurl-auth?tag=login&k1=... let lnurlAuthUrl = "lnurl1..." if case .lnurlAuth(v1: let requestData) = try await sdk.parse(input: lnurlAuthUrl) { print("Domain: \(requestData.domain)") print("Action: \(String(describing: requestData.action))") // Show domain to user and ask for confirmation // This is important for security } ``` ### Kotlin ```kotlin // LNURL-auth URL from a service // Can be in the form: // - lnurl1... (bech32 encoded) // - https://service.com/lnurl-auth?tag=login&k1=... val lnurlAuthUrl = "lnurl1..." when (val inputType = sdk.parse(lnurlAuthUrl)) { is InputType.LnurlAuth -> { val requestData = inputType.v1 println("Domain: ${requestData.domain}") println("Action: ${requestData.action}") // Show domain to user and ask for confirmation // This is important for security } else -> {} } ``` ### C# ```csharp // LNURL-auth URL from a service // Can be in the form: // - lnurl1... (bech32 encoded) // - https://service.com/lnurl-auth?tag=login&k1=... var lnurlAuthUrl = "lnurl1..."; var inputType = await sdk.Parse(lnurlAuthUrl); if (inputType is InputType.LnurlAuth lnurlAuth) { var requestData = lnurlAuth.v1; Console.WriteLine($"Domain: {requestData.domain}"); Console.WriteLine($"Action: {requestData.action}"); // Show domain to user and ask for confirmation // This is important for security } ``` ### Javascript (Wasm) ```typescript // LNURL-auth URL from a service // Can be in the form: // - lnurl1... (bech32 encoded) // - https://service.com/lnurl-auth?tag=login&k1=... const lnurlAuthUrl = 'lnurl1...' const inputType = await sdk.parse(lnurlAuthUrl) if (inputType.type === 'lnurlAuth') { console.log(`Domain: ${inputType.domain}`) console.log(`Action: ${inputType.action}`) // Show domain to user and ask for confirmation // This is important for security } ``` ### React Native ```typescript // LNURL-auth URL from a service // Can be in the form: // - lnurl1... (bech32 encoded) // - https://service.com/lnurl-auth?tag=login&k1=... const lnurlAuthUrl = 'lnurl1...' const inputType = await sdk.parse(lnurlAuthUrl) if (inputType.tag === InputType_Tags.LnurlAuth) { const requestData = inputType.inner[0] console.log(`Domain: ${requestData.domain}`) console.log(`Action: ${requestData.action}`) // Show domain to user and ask for confirmation // This is important for security } ``` ### Flutter ```dart // LNURL-auth URL from a service // Can be in the form: // - lnurl1... (bech32 encoded) // - https://service.com/lnurl-auth?tag=login&k1=... String lnurlAuthUrl = "lnurl1..."; InputType inputType = await sdk.parse(input: lnurlAuthUrl); if (inputType is InputType_LnurlAuth) { LnurlAuthRequestDetails requestData = inputType.field0; print("Domain: ${requestData.domain}"); print("Action: ${requestData.action}"); // Show domain to user and ask for confirmation // This is important for security } ``` ### Python ```python # LNURL-auth URL from a service # Can be in the form: # - lnurl1... (bech32 encoded) # - https://service.com/lnurl-auth?tag=login&k1=... lnurl_auth_url = "lnurl1..." try: input_type = await sdk.parse(lnurl_auth_url) if isinstance(input_type, InputType.LNURL_AUTH): request_data = input_type[0] logging.debug(f"Domain: {request_data.domain}") logging.debug(f"Action: {request_data.action}") # Show domain to user and ask for confirmation # This is important for security except Exception as error: logging.error(error) raise ``` ### Go ```go // LNURL-auth URL from a service // Can be in the form: // - lnurl1... (bech32 encoded) // - https://service.com/lnurl-auth?tag=login&k1=... lnurlAuthUrl := "lnurl1..." inputType, err := sdk.Parse(lnurlAuthUrl) if err == nil { if lnurlAuth, ok := inputType.(breez_sdk_spark.InputTypeLnurlAuth); ok { requestData := lnurlAuth.Field0 log.Printf("Domain: %s", requestData.Domain) log.Printf("Action: %v", requestData.Action) // Show domain to user and ask for confirmation // This is important for security } } ``` ## Performing Authentication Once you have the authentication request details, you can perform the authentication by passing the request to the `lnurl_auth` method. The SDK will: 1. Derive a domain-specific key pair 2. Sign the challenge with the derived key 3. Send the signature and public key to the service ### Rust ```rust // Perform LNURL authentication let result = sdk.lnurl_auth(request_data).await?; match result { LnurlCallbackStatus::Ok => { info!("Authentication successful"); } LnurlCallbackStatus::ErrorStatus { error_details } => { info!("Authentication failed: {}", error_details.reason); } } ``` ### Swift ```swift // Perform LNURL authentication let result = try await sdk.lnurlAuth(requestData: requestData) switch result { case .ok: print("Authentication successful") case .errorStatus(errorDetails: let errorDetails): print("Authentication failed: \(errorDetails.reason)") } ``` ### Kotlin ```kotlin // Perform LNURL authentication when (val result = sdk.lnurlAuth(requestData)) { is LnurlCallbackStatus.Ok -> { println("Authentication successful") } is LnurlCallbackStatus.ErrorStatus -> { println("Authentication failed: ${result.errorDetails.reason}") } } ``` ### C# ```csharp // Perform LNURL authentication var result = await sdk.LnurlAuth(requestData); if (result is LnurlCallbackStatus.Ok) { Console.WriteLine("Authentication successful"); } else if (result is LnurlCallbackStatus.ErrorStatus errorStatus) { Console.WriteLine($"Authentication failed: {errorStatus.errorDetails.reason}"); } ``` ### Javascript (Wasm) ```typescript // Perform LNURL authentication const result = await sdk.lnurlAuth(requestData) if (result.type === 'ok') { console.log('Authentication successful') } else if (result.type === 'errorStatus') { console.log(`Authentication failed: ${result.errorDetails.reason}`) } ``` ### React Native ```typescript // Perform LNURL authentication const result = await sdk.lnurlAuth(requestData) if (result.tag === LnurlCallbackStatus_Tags.Ok) { console.log('Authentication successful') } else if (result.tag === LnurlCallbackStatus_Tags.ErrorStatus) { console.log(`Authentication failed: ${result.inner.errorDetails.reason}`) } ``` ### Flutter ```dart // Perform LNURL authentication LnurlCallbackStatus result = await sdk.lnurlAuth(requestData: requestData); if (result is LnurlCallbackStatus_Ok) { print("Authentication successful"); } else if (result is LnurlCallbackStatus_ErrorStatus) { print("Authentication failed: ${result.errorDetails.reason}"); } ``` ### Python ```python # Perform LNURL authentication try: result = await sdk.lnurl_auth(request_data=request_data) if isinstance(result, LnurlCallbackStatus.OK): logging.debug("Authentication successful") elif isinstance(result, LnurlCallbackStatus.ERROR_STATUS): logging.debug(f"Authentication failed: {result.error_details.reason}") except Exception as error: logging.error(error) raise ``` ### Go ```go // Perform LNURL authentication result, err := sdk.LnurlAuth(requestData) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } log.Printf("Authentication error: %v", err) return } switch v := result.(type) { case breez_sdk_spark.LnurlCallbackStatusOk: log.Println("Authentication successful") case breez_sdk_spark.LnurlCallbackStatusErrorStatus: log.Printf("Authentication failed: %s", v.ErrorDetails.Reason) } ``` **Developer note** The SDK automatically derives domain-specific keys according to LUD-05, ensuring that each service gets a unique linking key. This protects user privacy by preventing services from correlating user identities across different domains. ## Action Types LNURL-Auth supports different action types that indicate the purpose of the authentication: - **register** - Create a new account - **login** - Sign in to an existing account - **link** - Link the Lightning wallet to an existing account - **auth** - Generic authentication Your application can use the `action` field to provide appropriate UI feedback to users. ## Security Considerations - Always verify the domain before authenticating - Show the domain to users for confirmation - The SDK derives unique keys per domain to prevent tracking - Authentication keys cannot be used to access funds ## Supported Specs - [LUD-01](https://github.com/lnurl/luds/blob/luds/01.md) LNURL bech32 encoding - [LUD-04](https://github.com/lnurl/luds/blob/luds/04.md) `auth` base spec - [LUD-05](https://github.com/lnurl/luds/blob/luds/05.md) BIP32-based seed generation for `auth` - [LUD-17](https://github.com/lnurl/luds/blob/luds/17.md) Support for lnurl auth --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Passkey login](/guide/passkey.md)** # Passkey login Passkey login lets users access their wallet using biometrics (fingerprint or face recognition) or a device PIN, eliminating the need to write down and safeguard a seed phrase. No keys or seed phrases are stored. The SDK uses the WebAuthn PRF extension to deterministically derive a seed phrase from the user's passkey on-demand, and regenerates it on each sign-in. One passkey can be associated with multiple wallets, each under its own label, discoverable across the user's devices through Nostr relays. - **[Setup](/guide/passkey_setup.md)** - host the Web / Android / iOS configuration files that tie passkeys to your app. - **[Onboarding](/guide/passkey_onboarding.md)** - initialize the client and wire up the sign-in, register, and unified onboarding flows. - **[Credential metadata](/guide/passkey_credential_metadata.md)** - pin a returning user, prevent duplicate registrations, and show the authenticator and sync status. - **[Managing labels](/guide/passkey_labels.md)** - derive multiple wallets from one passkey and discover them through Nostr. - **[PRF providers](/guide/passkey_prf_providers.md)** - use the built-in platform provider, or implement a custom one (hardware key, FIDO2, file-backed). ## Further reading - **[UX guidelines](/guide/uxguide_login.md)** - recommended onboarding UX, prompt counts, and error-recovery patterns. For the full technical specification, see the Passkey Login spec. **→ [Setup](/guide/passkey_setup.md)** # Setup Passkey Login uses a Relying Party (RP) to tie passkeys to your apps. Each platform you target (Web, Android, iOS / macOS) needs a configuration file declaring your app under the RP. Platform authenticators validate the RP association against these files before any WebAuthn ceremony. ## Hosting the configuration files Two ways to set up the RP: - **Shared with the Breez ecosystem (Breez-hosted).** A passkey registered in one Breez-registered app works in every other Breez-registered app on the same device, with no re-registration. [Contact us](mailto:contact@breez.technology?subject=Passkey%20configuration) to register your app, then pass `PasskeyProvider.BREEZ_RP_ID` as your `rpId`. - **Scoped to your ecosystem (self-hosted).** A passkey registered against your RP works across the apps and web origins you list in your configuration files. You host the well-known files yourself on an HTTPS domain you control. Pass that domain as your `rpId` (for example, `""`). Same code paths in either case; only the `rpId` value and who hosts the JSON differs. ## Web: Related Origins **Path**: `/.well-known/webauthn` ```json { "related_origins": [ "https://keys.breez.technology", "https://your-app.example.com" ] } ``` **Requirements**: Chrome 116+, Safari 18+, Edge 116+. HTTPS required (localhost exempt during development). **Related Origins: developer notes** **Firefox does not implement Related Origins.** Its users register fresh on each origin. For multi-domain support, host a separate RP ID per domain. **Chrome and Edge cap the number of distinct origins** in `related_origins` (around 5 per RP). For larger app families, partition into multiple RP IDs. **Browsers cache `.well-known/webauthn` aggressively.** Adding or removing an origin takes effect only after the cache TTL expires. ## Android: Asset Links **Path**: `/.well-known/assetlinks.json` ```json [ { "relation": [ "delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds" ], "target": { "namespace": "android_app", "package_name": "com.example.yourapp", "sha256_cert_fingerprints": [ "B6:16:AD:FE:C5:C6:D3:4C:93:01:5B:4A:79:20:21:4E:62:43:AB:29:28:EE:34:9A:F2:46:55:4B:54:FC:42:DF" ] } } ] ``` Replace `com.example.yourapp` with your application's package name and the fingerprint with your app's signing certificate SHA256. See the Digital Asset Links documentation and Credential Manager prerequisites. **Requirements**: Android 9+ (API 28) with Google Play Services, or Android 14+ (API 34) with any compatible authenticator. `compileSdkVersion` must be at least 34 (required by the `androidx.credentials` library, not the device). ## iOS / macOS: Apple App Site Association **Path**: `/.well-known/apple-app-site-association` ```json { "webcredentials": { "apps": [ "TEAMID.com.example.yourapp" ] } } ``` Replace `TEAMID` with your Apple Developer Team ID and `com.example.yourapp` with your bundle identifier. Your app must also declare the Associated Domains capability in Xcode (**Signing & Capabilities**, then **Associated Domains**, then add `webcredentials:`). **iOS / macOS: Associated Domains entitlement required** Without the Associated Domains entitlement declared in Xcode, passkey operations on iOS / macOS fail with a configuration error, even when `PasskeyClient.check_availability` returns `PasskeyAvailability::Available`. **iOS / macOS: Expo Managed Workflow** If you're using Expo, the Breez SDK plugin can configure the Associated Domains entitlement automatically. See the React Native/Expo installation guide for details on the enablePasskey option. **Requirements**: iOS 18.0+, macOS 15.0+. --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Onboarding](/guide/passkey_onboarding.md)** # Onboarding Initialize the `PasskeyClient`, then run the onboarding flow that fits your platform. ## Initialization `PasskeyClient` 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 `PasskeyProvider` for you, defaulting to the Breez shared RP (`keys.breez.technology`): a Breez-registered app needs only its Breez API key. Set `provider_options` 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 `PrfProvider` and inject it: ### Rust ```rust let prf_provider = Arc::new(CustomPrfProvider); // Fails when the config carries a proxy the relay transport cannot honour. let passkey = PasskeyClient::new(prf_provider, Some("".to_string()), None)?; ``` ### Swift ```swift // Throws when the config carries a proxy the relay transport cannot honour. let passkey = try PasskeyClient( breezApiKey: "", config: PasskeyConfig( providerOptions: PasskeyProviderOptions(rpId: "", rpName: "Your App") ) ) ``` ### Kotlin ```kotlin val passkey = PasskeyClient( breezApiKey = "", activityProvider = { activity }, config = PasskeyConfig( providerOptions = PasskeyProviderOptions(rpId = "", rpName = "Your App"), ), ) ``` ### C# ```csharp var prfProvider = new CustomPrfProvider(); return new PasskeyClient(prfProvider, "", null); ``` ### Javascript (Wasm) ```typescript const passkey = new PasskeyClient('', { providerOptions: { rpId: '', rpName: 'Your App' } }) ``` ### React Native ```typescript const passkey = new PasskeyClient( '', PasskeyConfig.create({ providerOptions: PasskeyProviderOptions.create({ rpId: '', rpName: 'Your App' }) }) ) ``` ### Flutter ```dart final passkey = PasskeyClient( breezApiKey: '', config: PasskeyConfig( providerOptions: PasskeyProviderOptions(rpId: '', rpName: 'Your App'), ), ); ``` ### Python ```python prf_provider = CustomPrfProvider() passkey = PasskeyClient(prf_provider, "", None) ``` ### Go ```go prfProvider := &CustomPrfProvider{} apiKey := "" // Fails when the config carries a proxy the relay transport cannot honour. return breez_sdk_spark.NewPasskeyClient(prfProvider, &apiKey, nil) ``` **Parameters:** | Parameter | Default | Description | |---|---|---| | `breez_api_key` | **required** | Your Breez API key, used to authenticate to the Breez relay for label storage. | | `default_label` | `"Default"` | Wallet label used when `PasskeyClient.register` / `PasskeyClient.sign_in` receive none. Set on `passkey_config`. | Configure the built-in provider through `provider_options` on `passkey_config` (a `PasskeyProviderOptions`): | Field | Default | Description | |---|---|---| | `rp_id` | Breez shared RP | Relying 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_name` | `"Breez"` | Display name for your app, shown in some authenticator UIs. | | `user_name` | `rp_name` | Account 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_name` | `user_name` | Human-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](/guide/passkey_prf_providers.md). ### Checking passkey availability Call `PasskeyClient.check_availability` 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 ```rust match passkey.check_availability().await? { PasskeyAvailability::Available => { // Passkey supported: proceed with connect_with_passkey. On web, // call PasskeyClient::supports_immediate_mediation to pick // single- vs two-button onboarding (native is always single). } 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 ```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 ```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# ```csharp 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 (Wasm) ```typescript 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 ```typescript 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 ```dart 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 ```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 ```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_passkey`. - **Web** uses the same unified flow where the browser supports immediate mediation, and two buttons ("Create a new passkey" / "Sign in with a passkey") otherwise. For explicit control over each path, call `PasskeyClient.sign_in` and `PasskeyClient.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 `credential` field carries whichever credential signed in or was registered. See [Credential metadata](/guide/passkey_credential_metadata.md) for using it. Call it without a label to support multiple wallets per passkey: `labels` then holds the returning user's full set (the response wallet is the default label). Show a picker when it has more than one entry and `PasskeyClient.sign_in` to the chosen label. #### Rust ```rust // Single-CTA onboarding: silent sign-in, fall through to register. // Without a label, a returning user's wallets are discovered in // `response.labels` (the response wallet is the default); a new user // gets a freshly registered default wallet. let response = passkey .connect_with_passkey(ConnectWithPasskeyRequest::default()) .await?; if response.labels.len() > 1 { // Multiple wallets: let the user pick, then sign_in to the chosen label. } let config = default_config(Network::Mainnet); let sdk = connect(ConnectRequest { config, seed: response.wallet.seed, storage_dir: "./.data".to_string(), }) .await?; ``` #### Swift ```swift // Single-CTA onboarding: silent sign-in, fall through to register. var config = defaultConfig(network: .mainnet) config.apiKey = "" let response = try await passkey.connectWithPasskey( request: ConnectWithPasskeyRequest() ) if response.labels.count > 1 { // Returning multi-wallet user: let them pick a label, then sign in to it. // let chosen = promptForLabel(response.labels) // return try await passkey.signIn(request: SignInRequest(label: chosen)) } let sdk = try await connect( request: ConnectRequest( config: config, seed: response.wallet.seed, storageDir: "./.data" )) ``` #### Kotlin ```kotlin // Single-CTA onboarding: silent sign-in, fall through to register. val config = defaultConfig(Network.MAINNET).apply { apiKey = "" } val response = passkey.connectWithPasskey(ConnectWithPasskeyRequest()) if (response.labels.size > 1) { // Returning multi-wallet user: let them pick a label and sign in to it. // val chosen = promptForLabel(response.labels) // return connect(ConnectRequest(config, passkey.signIn(SignInRequest(label = chosen)).wallet.seed, "./.data")) } val sdk = connect(ConnectRequest(config, response.wallet.seed, "./.data")) ``` #### C# ```csharp // 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() ); if (response.labels.Length > 1) { // Returning multi-wallet user: let them pick a label, then // SignIn to the chosen wallet. } var config = BreezSdkSparkMethods.DefaultConfig(network: Network.Mainnet); var sdk = await BreezSdkSparkMethods.Connect(new ConnectRequest( config: config, seed: response.wallet.seed, storageDir: "./.data" )); ``` #### Javascript (Wasm) ```typescript // Single-button flow. On web it works only where the browser supports // immediate mediation; supportsImmediateMediation() reports it. Otherwise // use the two-button flow (register / signIn). const availability = await passkey.checkAvailability() if (availability.type !== 'available' || !(await passkey.supportsImmediateMediation())) { throw new Error('Use the two-button flow (register / signIn) on this browser') } // No label: a returning user's wallets are discovered (response.labels, // with wallet being the default); a new user gets a freshly registered // default wallet. const response = await passkey.connectWithPasskey({}) if (response.labels.length > 1) { // Multiple wallets: let the user pick, then signIn to the chosen label. } const config = defaultConfig('mainnet') const sdk = await connect({ config, seed: response.wallet.seed, storageDir: './.data' }) ``` #### React Native ```typescript // Silent sign-in, fall through to register. No label: a returning user's // wallets come back in `response.labels` (the default is signed in). const config = { ...defaultConfig(Network.Mainnet), apiKey: '' } const response = await passkey.connectWithPasskey({ label: undefined, allowCredentials: undefined, excludeCredentials: undefined }) if (response.labels.length > 1) { // Returning multi-wallet user: let them pick a label and sign in to it. // const chosen = await pickLabel(response.labels) // return await passkey.signIn({ label: chosen, allowCredentials: undefined, preferImmediatelyAvailableCredentials: undefined }) } const sdk = await connect({ config, seed: response.wallet.seed, storageDir: './.data' }) ``` #### Flutter ```dart // Single-CTA onboarding: silent sign-in, fall through to register. final config = defaultConfig(network: Network.mainnet) .copyWith(apiKey: ''); final response = await passkey.connectWithPasskey( request: ConnectWithPasskeyRequest(), ); if (response.labels.length > 1) { // Returning multi-wallet user: let them pick a label, then sign in to it. // final chosen = await showWalletPicker(response.labels); // return (await passkey.signIn(request: SignInRequest(label: chosen))).wallet; } final sdk = await connect( request: ConnectRequest( config: config, seed: response.wallet.seed, storageDir: "./.data")); ``` #### Python ```python # Silent sign-in for a returning user, fall-through to register on a fresh device. # No label: derive the default wallet and discover this passkey's label set. response = await passkey.connect_with_passkey(ConnectWithPasskeyRequest()) if len(response.labels) > 1: # Returning multi-wallet user: let them pick a label and sign in to it. # chosen = ... # prompt the user with response.labels # response = await passkey.sign_in(SignInRequest(label=chosen)) pass config = default_config(network=Network.MAINNET) sdk = await connect( ConnectRequest(config=config, seed=response.wallet.seed, storage_dir="./.data") ) ``` #### Go ```go // Silent sign-in for a returning user, fall-through to register on a fresh device. // No label: derives the default wallet and discovers this passkey's full label set. response, err := passkey.ConnectWithPasskey(breez_sdk_spark.ConnectWithPasskeyRequest{}) if err != nil { return nil, err } if len(response.Labels) > 1 { // Returning multi-wallet user: let them pick a label and SignIn to it. // passkey.SignIn(breez_sdk_spark.SignInRequest{Label: &chosenLabel}) } 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 } ``` ### Web flow `PasskeyClient.connect_with_passkey` works on web too, **where the browser supports immediate mediation** (recent Chromium). Check `PasskeyClient.supports_immediate_mediation` and use the same single-button unified flow. Where it isn't supported (Safari, Firefox, older browsers), present two buttons: **Create a new passkey** (calls `PasskeyClient.register`) and **Sign in with a passkey** (calls `PasskeyClient.sign_in`). Without immediate mediation, WebAuthn reports "no credential" and "user cancelled" identically, so the SDK can't auto-detect the flow. ### Sign in and register Call `PasskeyClient.sign_in` and `PasskeyClient.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 `connect` in either case. #### Sign in Sign in to an existing credential: ##### Rust ```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 ```swift // Returning-user sign-in. No fall-through to register. return try await passkey.signIn(request: SignInRequest(label: "personal")) ``` ##### Kotlin ```kotlin // Returning-user sign-in. No fall-through to register. return passkey.signIn(SignInRequest(label = "personal")) ``` ##### Javascript (Wasm) ```typescript // Returning-user sign-in. No fall-through to register. return await passkey.signIn({ label: 'personal' }) ``` ##### React Native ```typescript // Returning-user sign-in. No fall-through to register. return await passkey.signIn({ label: 'personal', allowCredentials: undefined, preferImmediatelyAvailableCredentials: undefined }) ``` ##### Flutter ```dart // Returning-user sign-in. No fall-through to register. return await passkey.signIn(request: SignInRequest(label: 'personal')); ``` #### Register Register a fresh credential: ##### Rust ```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 ```swift var config = defaultConfig(network: .mainnet) config.apiKey = "" 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 ```kotlin val config = defaultConfig(Network.MAINNET).apply { apiKey = "" } val response = passkey.register(RegisterRequest(label = "personal")) val sdk = connect(ConnectRequest(config, response.wallet.seed, "./.data")) ``` ##### C# ```csharp 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 (Wasm) ```typescript const response = await passkey.register({ label: 'personal' }) const config = defaultConfig('mainnet') const sdk = await connect({ config, seed: response.wallet.seed, storageDir: './.data' }) ``` ##### React Native ```typescript const config = { ...defaultConfig(Network.Mainnet), apiKey: '' } const response = await passkey.register({ label: 'personal', excludeCredentials: undefined }) const sdk = await connect({ config, seed: response.wallet.seed, storageDir: './.data' }) ``` ##### Flutter ```dart final config = defaultConfig(network: Network.mainnet) .copyWith(apiKey: ''); final response = await passkey.register( request: RegisterRequest(label: 'personal'), ); final sdk = await connect( request: ConnectRequest( config: config, seed: response.wallet.seed, storageDir: "./.data")); ``` ##### Python ```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 ```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 Most passkey failures normalize to a `PrfProviderError` variant. Match on the variant to drive recovery: | Variant | What it means | Recommended action | |---|---|---| | `PrfProviderError::UserCancelled` | User dismissed the OS prompt | Sticky retry UI with "Try Again". | | `PrfProviderError::CredentialNotFound` | No matching credential on this device | Fall through to `PasskeyClient.register`. | | `PrfProviderError::CredentialAlreadyExists` | Register hit a credential in `exclude_credentials` | Flip to `PasskeyClient.sign_in`; the OS picker surfaces the existing credential. | | `PrfProviderError::UserTimedOut` | OS biometric inactivity timeout, distinct from a cancel | Sticky retry with timeout-specific copy. **Do not** auto-retry. | | `PrfProviderError::PrfNotSupported` | Authenticator lacks the PRF extension | Fall back to mnemonic onboarding. | | `PrfProviderError::Configuration` | Entitlement missing, AASA stale, or assetlinks malformed | Developer-facing error; surface the `PasskeyAvailability::NotAssociated` reason. | | `PrfProviderError::Generic` | Network or generic failure | Generic "try again later" UI. | Those rows cover `PasskeyClient.sign_in`, and `PasskeyClient.register` up to the point the credential is created. Once it exists, a failure from the authenticator arrives as the variant below instead: a cancel, a timeout or an unsupported authenticator during the derive no longer surfaces as its own `PrfProviderError`. `PasskeyClient.register` has one failure of its own that is **not** a `PrfProviderError`: | Variant | What it means | Recommended action | |---|---|---| | `PasskeyError::CreatedButNotDerived` | The passkey was created, then the authenticator failed the derive that followed | Sign in pinned to the `credential_id` on the error. **Do not** register again. | Handle it explicitly. The passkey exists on the device from that point on, so registering again leaves the first one behind owning a wallet nothing points to. A catch-all `else` branch will not fail to compile: it routes this into your generic "try again" path, which is usually a retry that registers a second passkey. It carries the underlying `PrfProviderError` as `source`, so unwrap once and reuse the arms above. Failures that are not the authenticator's (mnemonic, key derivation, invalid PRF output) keep their own variant and are not wrapped. 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 ```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 ```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 ```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# ```csharp 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 (Wasm) ```typescript 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 ```typescript 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 ```dart 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 ```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 ```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 ®isterResponse.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 ```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 ```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 ```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# ```csharp try { return await passkey.SignIn(new SignInRequest(label: "personal")); } catch (PrfProviderException.UserTimedOut) { Console.WriteLine("Sign-in timed out: show \"Try Again\" UI."); throw; } ``` ### Javascript (Wasm) ```typescript // 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 ```typescript // 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 ```dart // 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 ```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 ```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](/guide/uxguide_login.md) for the recommended recovery UX. ## Supported specs - [Seedless Restore](https://github.com/breez/seedless-restore): passkey-based wallet derivation and discovery - [Nostr](https://github.com/nostr-protocol/nostr): relay-based event protocol for label storage - [NIP-42](https://github.com/nostr-protocol/nips/blob/master/42.md): authentication of clients to relays - [NIP-65](https://github.com/nostr-protocol/nips/blob/master/65.md): relay list metadata --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Credential metadata](/guide/passkey_credential_metadata.md)** # Credential metadata Every passkey flow returns the credential it created or signed in with. Its IDs and attestation hints let you pin a returning user to the same wallet, prevent duplicate registrations, correlate with your backend, and show which authenticator holds the passkey. `register`, `sign_in`, and `connect_with_passkey` each return a `credential` field. The built-in platform providers always populate it; only PRF backends that don't surface one (CLI / file-backed / hardware) leave it unset. ## Fields `PasskeyCredential` carries: | Field | Available | Use it for | |---|---|---| | `credential_id` | always | [Pinning a returning user](#pin-a-returning-user-to-the-same-wallet) and [preventing duplicate registrations](#prevent-duplicate-registrations). | | `user_id` | registration | [Correlating with your backend](#correlate-the-credential-with-your-backend). | | `aaguid` | registration | [Showing the authenticator](#show-the-authenticator-and-sync-status). Unverified. | | `backup_eligible` | registration | [Showing the sync status](#show-the-authenticator-and-sync-status). | ## Using the fields Each of these is optional. The basic register and sign-in flows need none of them: reach for one only when your app wants that behavior. ### Pin a returning user to the same wallet Each credential derives its own wallet seed, so a returning user must sign in with the same credential to re-open the same wallet. Persist `credential_id` after registration and pass it as `allow_credentials` on `sign_in`. The OS then offers only that credential. Omit `allow_credentials` and the OS picks any matching credential for your RP. #### Rust ```rust let response = passkey .register(RegisterRequest { label: Some("personal".to_string()), ..Default::default() }) .await?; if let Some(credential) = &response.credential { // Persist to reopen the same wallet on sign-in println!("{:?}", credential.credential_id); // Authenticator model (display hint, unverified) println!("{:?}", credential.aaguid); // Whether the passkey syncs across devices println!("{:?}", credential.backup_eligible); } // Pin the stored credential ID so the OS can't substitute a sibling, // which would derive a different wallet. let sign_in_response = passkey .sign_in(SignInRequest { label: Some("personal".to_string()), allow_credentials: Some(vec![/* stored credential_id bytes */]), ..Default::default() }) .await?; // Pass to connect() to open the wallet println!("{:?}", sign_in_response.wallet.seed); // Label this wallet was derived from println!("{}", sign_in_response.wallet.label); // This passkey's labels (populated on discovery sign-in) println!("{:?}", sign_in_response.labels); // Credential signed in with (credential_id only) println!("{:?}", sign_in_response.credential); ``` #### Swift ```swift let response = try await passkey.register( request: RegisterRequest(label: "personal") ) if let credential = response.credential { // Persist to reopen the same wallet on sign-in print(credential.credentialId) // Authenticator model (display hint, unverified) print(credential.aaguid) // Whether the passkey syncs across devices print(credential.backupEligible) } // Pin the stored credential ID so the OS can't substitute a sibling // credential, which would derive a different wallet. let signInResponse = try await passkey.signIn( request: SignInRequest( label: "personal", allowCredentials: [ // stored credentialId bytes ] ) ) // Pass to connect() to open the wallet print(signInResponse.wallet.seed) // Label this wallet was derived from print(signInResponse.wallet.label) // This passkey's labels (populated on discovery sign-in) print(signInResponse.labels) // Credential signed in with (credential_id only) print(signInResponse.credential) ``` #### Kotlin ```kotlin val response = passkey.register(RegisterRequest(label = "personal")) response.credential?.let { credential -> // Log.v("Breez", "${credential.credentialId}") // Persist to reopen the same wallet on sign-in // Log.v("Breez", "${credential.aaguid}") // Authenticator model (display hint, unverified) // Log.v("Breez", "${credential.backupEligible}") // Whether the passkey syncs across devices } // Pin the stored credential ID so the OS can't substitute a sibling // credential, which would derive a different wallet. val signInResponse = passkey.signIn( SignInRequest( label = "personal", // stored credentialId bytes allowCredentials = emptyList(), ) ) // Log.v("Breez", "${signInResponse.wallet.seed}") // Pass to connect() to open the wallet // Log.v("Breez", "${signInResponse.wallet.label}") // Label this wallet was derived from // Log.v("Breez", "${signInResponse.labels}") // This passkey's labels (populated on discovery sign-in) // Log.v("Breez", "${signInResponse.credential}") // Credential signed in with (credential_id only) ``` #### C# ```csharp var response = await passkey.Register(new RegisterRequest(label: "personal")); if (response.credential is not null) { // Persist to reopen the same wallet on sign-in Console.WriteLine(response.credential.credentialId); // Authenticator model (display hint, unverified) Console.WriteLine(response.credential.aaguid); // Whether the passkey syncs across devices Console.WriteLine(response.credential.backupEligible); } // Pin the stored credential ID so the OS can't substitute a sibling // credential, which would derive a different wallet. var signInResponse = await passkey.SignIn(new SignInRequest( label: "personal", allowCredentials: new byte[][] { // stored credentialId bytes } )); // Pass to connect() to open the wallet Console.WriteLine(signInResponse.wallet.seed); // Label this wallet was derived from Console.WriteLine(signInResponse.wallet.label); // This passkey's labels (populated on discovery sign-in) Console.WriteLine(signInResponse.labels); // Credential signed in with (credential_id only) Console.WriteLine(signInResponse.credential); ``` #### Javascript (Wasm) ```typescript const response = await passkey.register({ label: 'personal' }) if (response.credential != null) { // Persist to reopen the same wallet on sign-in console.log(response.credential.credentialId) // Authenticator model (display hint, unverified) console.log(response.credential.aaguid) // Whether the passkey syncs across devices console.log(response.credential.backupEligible) } // Pin the stored credential ID so the OS can't substitute a sibling // credential, which would derive a different wallet. const signInResponse = await passkey.signIn({ label: 'personal', allowCredentials: [/* stored credentialId bytes */] }) // Pass to connect() to open the wallet console.log(signInResponse.wallet.seed) // Label this wallet was derived from console.log(signInResponse.wallet.label) // This passkey's labels (populated on discovery sign-in) console.log(signInResponse.labels) // Credential signed in with (credential_id only) console.log(signInResponse.credential) ``` #### React Native ```typescript const response = await passkey.register({ label: 'personal', excludeCredentials: undefined }) if (response.credential !== undefined) { // Persist to reopen the same wallet on sign-in console.log(response.credential.credentialId) // Authenticator model (display hint, unverified) console.log(response.credential.aaguid) // Whether the passkey syncs across devices console.log(response.credential.backupEligible) } // Pin the stored credential ID so the OS can't substitute a sibling // credential, which would derive a different wallet. const signInResponse = await passkey.signIn({ label: 'personal', allowCredentials: [/* stored credentialId bytes */], preferImmediatelyAvailableCredentials: undefined }) // Pass to connect() to open the wallet console.log(signInResponse.wallet.seed) // Label this wallet was derived from console.log(signInResponse.wallet.label) // This passkey's labels (populated on discovery sign-in) console.log(signInResponse.labels) // Credential signed in with (credential_id only) console.log(signInResponse.credential) ``` #### Flutter ```dart final response = await passkey.register( request: RegisterRequest(label: 'personal'), ); final credential = response.credential; if (credential != null) { // Persist to reopen the same wallet on sign-in print(credential.credentialId); // Authenticator model (display hint, unverified) print(credential.aaguid); // Whether the passkey syncs across devices print(credential.backupEligible); } // Pin the stored credential ID so the OS can't substitute a sibling. final signInResponse = await passkey.signIn( request: SignInRequest(label: 'personal', allowCredentials: const [ // stored credentialId bytes ]), ); // Pass to connect() to open the wallet print(signInResponse.wallet.seed); // Label this wallet was derived from print(signInResponse.wallet.label); // This passkey's labels (populated on discovery sign-in) print(signInResponse.labels); // Credential signed in with (credential_id only) print(signInResponse.credential); ``` #### Python ```python response = await passkey.register(RegisterRequest(label="personal")) if response.credential is not None: # Persist to reopen the same wallet on sign-in print(response.credential.credential_id) # Authenticator model (display hint, unverified) print(response.credential.aaguid) # Whether the passkey syncs across devices print(response.credential.backup_eligible) # Pin the stored credential ID so the OS can't substitute a sibling # credential, which would derive a different wallet. sign_in_response = await passkey.sign_in( SignInRequest( label="personal", allow_credentials=[ # stored credential_id bytes ], ) ) # Pass to connect() to open the wallet print(sign_in_response.wallet.seed) # Label this wallet was derived from print(sign_in_response.wallet.label) # This passkey's labels (populated on discovery sign-in) print(sign_in_response.labels) # Credential signed in with (credential_id only) print(sign_in_response.credential) ``` #### Go ```go label := "personal" response, err := passkey.Register(breez_sdk_spark.RegisterRequest{Label: &label}) if err != nil { return err } if response.Credential != nil { // Persist to reopen the same wallet on sign-in log.Println(response.Credential.CredentialId) // Authenticator model (display hint, unverified) log.Println(response.Credential.Aaguid) // Whether the passkey syncs across devices log.Println(response.Credential.BackupEligible) } // Pin the stored credential ID so the OS can't substitute a sibling // credential, which would derive a different wallet. signInResponse, err := passkey.SignIn(breez_sdk_spark.SignInRequest{ Label: &label, AllowCredentials: &[][]byte{ // stored CredentialId bytes }, }) if err != nil { return err } // Pass to connect() to open the wallet log.Println(signInResponse.Wallet.Seed) // Label this wallet was derived from log.Println(signInResponse.Wallet.Label) // This passkey's labels (populated on discovery sign-in) log.Println(signInResponse.Labels) // Credential signed in with (credential_id only) log.Println(signInResponse.Credential) ``` ### Prevent duplicate registrations Pass the user's already-registered credential IDs as `exclude_credentials` on `register`. When one is already on the device, the OS refuses to create a second and raises `PrfProviderError::CredentialAlreadyExists`: route that to `sign_in` so the picker surfaces the existing credential. #### Rust ```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 ```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 ```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# ```csharp 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 (Wasm) ```typescript 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 ```typescript 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 ```dart 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 ```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 ```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 ®isterResponse.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 ``` ### Correlate the credential with your backend If your backend ties passkeys to your own user accounts, `user_id` is a stable identifier set at registration that links the two. The SDK surfaces it locally and never transmits it. Persist it with your user record, then match it on later sign-ins to tell which user is signing in. This enables account-level controls the passkey layer can't enforce on its own: - Cap how many passkeys (and wallets) one account may register. - Revoke a lost credential server-side. - List a user's registered devices in their settings. ### Show the authenticator and sync status `aaguid` identifies the authenticator that created the passkey (Apple Passwords, Google Password Manager, a hardware key). Look it up in the community [AAGUID database](https://github.com/passkeydeveloper/passkey-authenticator-aaguids) for a name and icon. `backup_eligible` tells you whether the passkey syncs across the user's devices. > **Note:** `aaguid` and `backup_eligible` are unverified and self-reported by the authenticator. Use them as display hints, never as a trust signal. ## Persisting the values The use cases above require these values to be persisted across app launches. `credential_id` is returned on every authentication response, while `aaguid`, `backup_eligible`, and `user_id` are only returned during registration and should be stored at that time. Use synced storage such as iCloud Keychain (iOS), Block Store (Android), or your own synced backend. Local-only storage is insufficient because it is lost on app reinstall and cannot be accessed from another device. --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Managing labels](/guide/passkey_labels.md)** # Managing labels Labels distinguish wallets derived from the same passkey identity. `PasskeyClient.register` and `PasskeyClient.sign_in` manage them implicitly, while `PasskeyClient.labels` gives you direct access to the underlying list and publish operations. Both calls prompt the user for a passkey ceremony. ## Listing Fetch the labels registered for the passkey from Nostr. ### Rust ```rust let labels = passkey.labels().list().await?; for label in &labels { println!("Found label: {label}"); } ``` ### Swift ```swift let labels = try await passkey.labels().list() for label in labels { print("Found label: \(label)") } ``` ### Kotlin ```kotlin val labels = passkey.labels().list() for (label in labels) { // Log.v("Breez", "Found label: $label") } ``` ### C# ```csharp var labels = await passkey.Labels().List(); foreach (var label in labels) { Console.WriteLine($"Found label: {label}"); } ``` ### Javascript (Wasm) ```typescript const labels = await passkey.labels().list() for (const label of labels) { console.log(`Found label: ${label}`) } ``` ### React Native ```typescript const labels = await passkey.labels().list() for (const label of labels) { console.log(`Found label: ${label}`) } ``` ### Flutter ```dart final labels = await passkey.labels().list(); for (final label in labels) { print("Found label: $label"); } ``` ### Python ```python labels = await passkey.labels().list() for label in labels: print(f"Found label: {label}") ``` ### Go ```go labels, err := passkey.Labels().List() if err != nil { return nil, err } for _, label := range labels { log.Printf("Found label: %s", label) } ``` ## Storing Publish a label to Nostr so it can be discovered later. ### Rust ```rust passkey.labels().store("personal".to_string()).await?; ``` ### Swift ```swift try await passkey.labels().store(label: "personal") ``` ### Kotlin ```kotlin passkey.labels().store("personal") ``` ### C# ```csharp await passkey.Labels().Store(label: "personal"); ``` ### Javascript (Wasm) ```typescript await passkey.labels().store('personal') ``` ### React Native ```typescript await passkey.labels().store('personal') ``` ### Flutter ```dart await passkey.labels().store(label: "personal"); ``` ### Python ```python await passkey.labels().store(label="personal") ``` ### Go ```go err := passkey.Labels().Store("personal") if err != nil { return err } ``` --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [PRF providers](/guide/passkey_prf_providers.md)** # PRF providers The built-in `PasskeyProvider` covers the common case. Reach for this page when: - You need platform-specific provider options (iOS `URLSession` / presentation anchor, Android `Activity` wiring, web `authenticatorAttachment`). - You're integrating Python, Go, or C# (no built-in `PasskeyProvider` ships for those bindings). - You need a custom `PrfProvider` (CLI YubiKey, FIDO2, air-gapped backup file, hardware module). ## Built-in PasskeyProvider options The built-in `PasskeyProvider` takes a `PasskeyProviderOptions`: | Field | Default | Description | |--------|---------|-------------| | `rp_id` | Breez shared RP | Relying Party ID. Your app's domain, or `PasskeyProvider.BREEZ_RP_ID` (`keys.breez.technology`) if Breez-registered. Changing it makes existing passkeys derive a different seed (see [migration considerations](https://github.com/breez/passkey-login/blob/main/SDK%20implementation.md#passkey-migration-considerations)). | | `rp_name` | `"Breez"` | Display name for your app, shown in some authenticator UIs. Registration-only. | | `user_name` | `rp_name` | Account identifier shown beneath the display name in the OS picker, e.g. `john@doe.com`. Pass a stable per-user value so each registration is a distinct entry (Apple Passwords dedupes by `(rpId, user.name)`). Registration-only. | | `user_display_name` | `user_name` | Human-friendly name shown most prominently, e.g. `John Doe`. Registration-only. | The same `PasskeyProviderOptions` is settable on `passkey_config` via `provider_options`, which builds the provider for you (see [Initialization](/guide/passkey_onboarding.md#initialization)). Construct `PasskeyProvider` directly only for platform-specific options (iOS `URLSession`, web `authenticatorAttachment`) or a custom backend. **C# / Go / Python limitation** The SDK does not ship a built-in `PasskeyProvider` for C#, Go, or Python (no native passkey API to wrap). On those bindings, implement your own `PrfProvider` and pass it to `PasskeyClient`. ## Custom PrfProvider To support a custom authenticator (hardware security key, FIDO2/CTAP2 transport, air-gapped backup file), implement the `PrfProvider` interface directly. The Breez CLI ships [YubiKey](https://github.com/breez/spark-sdk/blob/main/crates/breez-sdk/cli/src/passkey/yubikey_prf.rs), [FIDO2](https://github.com/breez/spark-sdk/blob/main/crates/breez-sdk/cli/src/passkey/fido2_prf.rs), and [file-based](https://github.com/breez/spark-sdk/blob/main/crates/breez-sdk/cli/src/passkey/file_prf.rs) implementations as references. ### Rust ```rust /// Implement PrfProvider for a custom authenticator (hardware key, FIDO2, /// file-backed). Only derive_seeds and is_supported are required. struct CustomPrfProvider; #[async_trait::async_trait] impl PrfProvider for CustomPrfProvider { async fn derive_seeds( &self, _request: DeriveSeedsRequest, ) -> Result { // Return one 32-byte PRF output per salt, in input order. todo!("Implement using WebAuthn or native passkey APIs") } async fn is_supported(&self) -> Result { todo!("Check platform passkey availability") } async fn create_passkey( &self, _exclude_credentials: Vec>, _salts: Vec, ) -> Result { // Register a credential and return its ID plus attestation. // // Return `seeds: None` unless the platform evaluated PRF during the // create ceremony and gave one output per salt. Seeds returned here // must equal what `derive_seeds` returns for the same salts. todo!("Implement registration via WebAuthn create() / native API") } async fn check_domain_association(&self) -> Result { // Custom providers without a verification source return Skipped. Ok(DomainAssociation::Skipped { reason: "CustomPrfProvider does not verify domain association".to_string(), }) } } ``` ### Swift ```swift // Implement PrfProvider for a custom authenticator (hardware key, FIDO2, // file-backed). Every method is required: Swift conformance has no // defaults, unlike the Rust trait. class CustomPrfProvider: PrfProvider { func deriveSeeds(request: DeriveSeedsRequest) async throws -> DeriveSeedsOutput { // Return one 32-byte PRF output per salt, in input order. fatalError("Implement using WebAuthn or native passkey APIs") } func isSupported() async throws -> Bool { fatalError("Check platform passkey availability") } func createPasskey( excludeCredentials: [Data], salts: [String] ) async throws -> CreatePasskeyOutput { // Register a credential and return its ID plus attestation. // // Return `seeds: nil` unless the platform evaluated PRF during the // create ceremony and gave one output per salt. Seeds returned here // must equal what `deriveSeeds` returns for the same salts. fatalError("Implement registration via WebAuthn create() / native API") } func checkDomainAssociation() async throws -> DomainAssociation { return .skipped(reason: "CustomPrfProvider does not verify domain association") } } ``` ### Kotlin ```kotlin // Implement PrfProvider for a custom authenticator. Every method is // required: the generated interface has no defaults, unlike the Rust trait. class CustomPrfProvider : PrfProvider { override suspend fun deriveSeeds(request: DeriveSeedsRequest): DeriveSeedsOutput { // Return one 32-byte PRF output per salt, in input order. TODO("Implement using WebAuthn or native passkey APIs") } override suspend fun isSupported(): Boolean { TODO("Check platform passkey availability") } override suspend fun createPasskey( excludeCredentials: List, salts: List, ): CreatePasskeyOutput { // Register a credential and return its ID plus attestation. // // Return `seeds = null` unless the platform evaluated PRF during the // create ceremony and gave one output per salt. Seeds returned here // must equal what `deriveSeeds` returns for the same salts. TODO("Implement registration via native passkey API") } override suspend fun checkDomainAssociation(): DomainAssociation { return DomainAssociation.Skipped("CustomPrfProvider does not verify domain association") } } ``` ### C# ```csharp // Implement PrfProvider for a custom authenticator (hardware key, FIDO2, // file-backed). Every method is required: interface members are // bodiless, unlike the Rust trait's defaults. class CustomPrfProvider : PrfProvider { public async Task DeriveSeeds(DeriveSeedsRequest request) { // Return one 32-byte PRF output per salt, in input order. throw new NotImplementedException("Implement using WebAuthn or native passkey APIs"); } public async Task IsSupported() { throw new NotImplementedException("Check platform passkey availability"); } public async Task CreatePasskey(byte[][] excludeCredentials, string[] salts) { // Register a credential and return its ID plus attestation. // // Return a null Seeds unless the platform evaluated PRF during the // create ceremony and gave one output per salt. Seeds returned here // must equal what DeriveSeeds returns for the same salts. throw new NotImplementedException("Implement registration via native passkey API"); } public async Task CheckDomainAssociation() { return await Task.FromResult( new DomainAssociation.Skipped("CustomPrfProvider does not verify domain association")); } } ``` ### Javascript (Wasm) ```typescript // Implement PrfProvider for a custom authenticator (hardware key, FIDO2, // file-backed). Only deriveSeeds and isSupported are required. class CustomPrfProvider { deriveSeeds = async ( salts: string[] ): Promise<{ seeds: Uint8Array[], credentialId: Uint8Array | null }> => { // Return one 32-byte PRF output per salt, in input order. throw new Error('Implement using WebAuthn or native passkey APIs') } createPasskey = async ( _excludeCredentials: Uint8Array[], _salts: string[] ): Promise => { // Register a credential and return its ID plus attestation. // // Return `seeds: null` unless the browser evaluated PRF during the // create ceremony and gave one output per salt. Seeds returned here // must equal what `deriveSeeds` returns for the same salts. throw new Error('Implement registration via WebAuthn create() / native API') } isSupported = async (): Promise => { throw new Error('Check platform passkey availability') } } ``` ### React Native ```typescript // Implement PrfProvider for a custom authenticator (hardware key, FIDO2, // file-backed). Every method is required: the generated interface has no // defaults, unlike the Rust trait. class CustomPrfProvider { deriveSeeds = async ( _request: { salts: string[] } ): Promise<{ seeds: Uint8Array[], credentialId?: Uint8Array }> => { // Return one 32-byte PRF output per salt, in input order. throw new Error('Implement using WebAuthn or native passkey APIs') } createPasskey = async ( _excludeCredentials: Uint8Array[], _salts: string[] ): Promise => { // Register a credential and return its ID plus attestation. // // Return `seeds: null` (the field is nullable) unless the platform evaluated PRF during the // create ceremony and gave one output per salt. Seeds returned here // must equal what `deriveSeeds` returns for the same salts. throw new Error('Implement registration via native passkey API') } isSupported = async (): Promise => { throw new Error('Check platform passkey availability') } } ``` ### Flutter ```dart // Implement custom callbacks if the built-in PasskeyProvider doesn't // fit your needs. Pass them to PasskeyClient.fromCallbacks instead // of going through PasskeyClientBuilder.withPrfProvider. Future deriveSeeds(DeriveSeedsRequest request) async { // Return one 32-byte PRF output per salt, in input order. throw UnimplementedError('Implement using platform passkey APIs'); } Future createPasskey( List excludeCredentials, List salts, ) async { // Register a credential and return its ID plus attestation. // // Return `seeds` null unless the platform evaluated PRF during the create // ceremony and gave one output per salt. Seeds returned here must equal // what `deriveSeeds` returns for the same salts. throw UnimplementedError('Implement registration via native passkey API'); } Future isSupported() async { throw UnimplementedError('Check platform passkey availability'); } ``` ### Python ```python # Implement the PrfProvider trait for custom logic if no built-in # PasskeyProvider ships for your target. Every method is required: # derive_seeds for derivation, is_supported for the capability probe, # create_passkey for registration, check_domain_association for the # advisory RP check. class CustomPrfProvider(PrfProvider): async def derive_seeds(self, request: DeriveSeedsRequest) -> DeriveSeedsOutput: # Return one 32-byte PRF output per salt, in input order. raise NotImplementedError("Implement using WebAuthn or native passkey APIs") async def is_supported(self) -> bool: raise NotImplementedError("Check platform passkey availability") async def create_passkey( self, exclude_credentials: list[bytes], salts: list[str] ) -> CreatePasskeyOutput: # Register a credential and return its ID plus attestation. # # Return seeds=None unless the platform evaluated PRF during the create # ceremony and gave one output per salt. Seeds returned here must equal # what derive_seeds returns for the same salts. raise NotImplementedError("Implement registration via native passkey API") async def check_domain_association(self) -> DomainAssociation: # Optional: verify the app's identity against the platform's # domain verification source. Custom providers without a # verification source return SKIPPED, which tells callers # "proceed with WebAuthn as normal". The UniFFI-generated # variant classes are reparented to DomainAssociation at # runtime but mypy can't see that, hence the cast. return cast( DomainAssociation, DomainAssociation.SKIPPED( reason="CustomPrfProvider does not verify domain association" ), ) ``` ### Go ```go // Implement the PrfProvider interface for a custom authenticator (hardware // key, FIDO2, file-backed). Every method is required: satisfying a Go // interface has no defaults, unlike the Rust trait. type CustomPrfProvider struct{} func (p *CustomPrfProvider) DeriveSeeds( request breez_sdk_spark.DeriveSeedsRequest, ) (breez_sdk_spark.DeriveSeedsOutput, error) { // Return one 32-byte PRF output per salt, in input order. panic("Implement using WebAuthn or native passkey APIs") } func (p *CustomPrfProvider) IsSupported() (bool, error) { panic("Check platform passkey availability") } func (p *CustomPrfProvider) CreatePasskey( excludeCredentials [][]byte, salts []string, ) (breez_sdk_spark.CreatePasskeyOutput, error) { // Register a credential and return its ID plus attestation. // // Return a nil Seeds unless the platform evaluated PRF during the create // ceremony and gave one output per salt. Seeds returned here must equal // what DeriveSeeds returns for the same salts. panic("Implement registration via native passkey API") } func (p *CustomPrfProvider) CheckDomainAssociation() (breez_sdk_spark.DomainAssociation, error) { return breez_sdk_spark.DomainAssociationSkipped{ Reason: "CustomPrfProvider does not verify domain association", }, nil } ``` --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Stable balance](/guide/stable_balance.md)** # Stable Balance The stable balance feature enables users to switch between bitcoin and a stablecoin, like USDB, protecting against bitcoin price volatility. On receive, sats are automatically converted to the stablecoin. On send, the stablecoin is converted to Bitcoin. ## How it works When Stable Balance is configured and activated, for example with USDB, the SDK manages conversions in both directions using [token conversions](/guide/token_conversion.md): - **On receive** — When you receive a payment (Lightning, Spark, or on-chain), the SDK converts the incoming sats to USDB once your sats balance exceeds the configured threshold. - **On send** — When you send a bitcoin payment and your sats balance is insufficient, the SDK converts USDB back to bitcoin to cover the payment. See [Sending payments with stable balance](#sending-payments-with-stable-balance) for more details. Your balance remains stable in value, denominated in USD. ## Configuration To enable stable balance, configure the [stable balance config](/guide/config.md#stable-balance-configuration) when initializing the SDK: - **Tokens** — The stablecoin to use. Specify its token identifier and a display label. - **Default Active Label** — Optional label to activate by default. If unset, Stable Balance starts deactivated and can be activated at runtime via [user settings](/guide/user_settings.md). - **Threshold Sats** — Optional minimum sats balance to trigger automatic conversion. We recommend omitting this to use the conversion limit minimum. - **Maximum Slippage** — Optional maximum slippage in basis points. We recommend omitting this to use the default of 10 bps (0.1%). ### Rust ```rust let mut config = default_config(Network::Mainnet); // Enable stable balance with USDB conversion config.stable_balance_config = Some(StableBalanceConfig { tokens: vec![StableBalanceToken { label: "USDB".to_string(), token_identifier: "btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87" .to_string(), }], default_active_label: Some("USDB".to_string()), threshold_sats: None, max_slippage_bps: None, }); ``` ### Swift ```swift var config = defaultConfig(network: Network.mainnet) // Enable stable balance with USDB conversion config.stableBalanceConfig = StableBalanceConfig( tokens: [StableBalanceToken( label: "USDB", tokenIdentifier: "btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87" )], defaultActiveLabel: "USDB" ) ``` ### Kotlin ```kotlin val config = defaultConfig(Network.MAINNET) // Enable stable balance with USDB conversion config.stableBalanceConfig = StableBalanceConfig( tokens = listOf(StableBalanceToken( label = "USDB", tokenIdentifier = "btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87", )), defaultActiveLabel = "USDB", ) ``` ### C# ```csharp var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { // Enable stable balance with USDB conversion stableBalanceConfig = new StableBalanceConfig( tokens: new StableBalanceToken[] { new StableBalanceToken( label: "USDB", tokenIdentifier: "btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87" ) }, defaultActiveLabel: "USDB" ) }; ``` ### Javascript (Wasm) ```typescript const config = defaultConfig('mainnet') // Enable stable balance with USDB conversion config.stableBalanceConfig = { tokens: [{ label: 'USDB', tokenIdentifier: 'btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87' }], defaultActiveLabel: 'USDB' } ``` ### React Native ```typescript const config = defaultConfig(Network.Mainnet) // Enable stable balance with USDB conversion config.stableBalanceConfig = { tokens: [{ label: 'USDB', tokenIdentifier: 'btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87' }], defaultActiveLabel: 'USDB', thresholdSats: undefined, maxSlippageBps: undefined } ``` ### Flutter ```dart var config = defaultConfig(network: Network.mainnet).copyWith( // Enable stable balance with USDB conversion stableBalanceConfig: StableBalanceConfig( tokens: [StableBalanceToken( label: "USDB", tokenIdentifier: "btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87", )], defaultActiveLabel: "USDB", )); ``` ### Python ```python config = default_config(network=Network.MAINNET) # Enable stable balance with USDB conversion config.stable_balance_config = StableBalanceConfig( tokens=[StableBalanceToken( label="USDB", token_identifier="btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87", )], default_active_label="USDB", ) ``` ### Go ```go config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) // Enable stable balance with USDB conversion defaultActiveLabel := "USDB" stableBalanceConfig := breez_sdk_spark.StableBalanceConfig{ Tokens: []breez_sdk_spark.StableBalanceToken{ { Label: "USDB", TokenIdentifier: "btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87", }, }, DefaultActiveLabel: &defaultActiveLabel, } config.StableBalanceConfig = &stableBalanceConfig ``` **Developer note** If the configured `threshold sats` is lower than the minimum amount required by the conversion protocol, the protocol minimum will be used instead. This ensures conversions always meet the minimum requirements. ## Switching Stable Balance mode You can activate, switch, or deactivate Stable Balance at runtime using the [user settings](/guide/user_settings.md) API. This allows users to choose when to enable Stable Balance and which stablecoin to use. ### Activating Stable Balance To activate Stable Balance, set the active label to one of the labels defined in your #{{name StableBalanceConfig.tokens}} list: #### Rust ```rust sdk.update_user_settings(UpdateUserSettingsRequest { spark_private_mode_enabled: None, stable_balance_active_label: Some(StableBalanceActiveLabel::Set { label: "USDB".to_string(), }), spark_master_identity_public_key: None, }) .await?; ``` #### Swift ```swift try await sdk.updateUserSettings( request: UpdateUserSettingsRequest( sparkPrivateModeEnabled: nil, stableBalanceActiveLabel: .set(label: "USDB") )) ``` #### Kotlin ```kotlin try { sdk.updateUserSettings(UpdateUserSettingsRequest( sparkPrivateModeEnabled = null, stableBalanceActiveLabel = StableBalanceActiveLabel.Set(label = "USDB") )) } catch (e: Exception) { // handle error } ``` #### C# ```csharp await sdk.UpdateUserSettings( request: new UpdateUserSettingsRequest( sparkPrivateModeEnabled: null, stableBalanceActiveLabel: new StableBalanceActiveLabel.Set(label: "USDB") ) ); ``` #### Javascript (Wasm) ```typescript await sdk.updateUserSettings({ stableBalanceActiveLabel: { type: 'set', label: 'USDB' } }) ``` #### React Native ```typescript await sdk.updateUserSettings({ sparkPrivateModeEnabled: undefined, stableBalanceActiveLabel: new StableBalanceActiveLabel.Set({ label: 'USDB' }), sparkMasterIdentityPublicKey: undefined }) ``` #### Flutter ```dart await sdk.updateUserSettings( request: UpdateUserSettingsRequest( stableBalanceActiveLabel: StableBalanceActiveLabel_Set(label: "USDB"))); ``` #### Python ```python try: await sdk.update_user_settings( request=UpdateUserSettingsRequest( spark_private_mode_enabled=None, stable_balance_active_label=StableBalanceActiveLabel.SET(label="USDB") ) ) except Exception as error: logging.error(error) raise ``` #### Go ```go activeLabel := breez_sdk_spark.StableBalanceActiveLabel( breez_sdk_spark.StableBalanceActiveLabelSet{Label: "USDB"}, ) err := sdk.UpdateUserSettings(breez_sdk_spark.UpdateUserSettingsRequest{ StableBalanceActiveLabel: &activeLabel, }) if err != nil { return err } ``` When activated, the SDK immediately converts any excess sats balance to the specified token. ### Deactivating Stable Balance To deactivate Stable Balance, unset the active label: #### Rust ```rust sdk.update_user_settings(UpdateUserSettingsRequest { spark_private_mode_enabled: None, stable_balance_active_label: Some(StableBalanceActiveLabel::Unset), spark_master_identity_public_key: None, }) .await?; ``` #### Swift ```swift try await sdk.updateUserSettings( request: UpdateUserSettingsRequest( sparkPrivateModeEnabled: nil, stableBalanceActiveLabel: .unset )) ``` #### Kotlin ```kotlin try { sdk.updateUserSettings(UpdateUserSettingsRequest( sparkPrivateModeEnabled = null, stableBalanceActiveLabel = StableBalanceActiveLabel.Unset )) } catch (e: Exception) { // handle error } ``` #### C# ```csharp await sdk.UpdateUserSettings( request: new UpdateUserSettingsRequest( sparkPrivateModeEnabled: null, stableBalanceActiveLabel: new StableBalanceActiveLabel.Unset() ) ); ``` #### Javascript (Wasm) ```typescript await sdk.updateUserSettings({ stableBalanceActiveLabel: { type: 'unset' } }) ``` #### React Native ```typescript await sdk.updateUserSettings({ sparkPrivateModeEnabled: undefined, stableBalanceActiveLabel: new StableBalanceActiveLabel.Unset(), sparkMasterIdentityPublicKey: undefined }) ``` #### Flutter ```dart await sdk.updateUserSettings( request: UpdateUserSettingsRequest( stableBalanceActiveLabel: StableBalanceActiveLabel_Unset())); ``` #### Python ```python try: await sdk.update_user_settings( request=UpdateUserSettingsRequest( spark_private_mode_enabled=None, stable_balance_active_label=StableBalanceActiveLabel.UNSET() ) ) except Exception as error: logging.error(error) raise ``` #### Go ```go activeLabel := breez_sdk_spark.StableBalanceActiveLabel( breez_sdk_spark.StableBalanceActiveLabelUnset{}, ) err := sdk.UpdateUserSettings(breez_sdk_spark.UpdateUserSettingsRequest{ StableBalanceActiveLabel: &activeLabel, }) if err != nil { return err } ``` When deactivated, the SDK converts any remaining token balance back to Bitcoin. ### Checking the current mode You can check which token is currently active using `get_user_settings`: #### Rust ```rust let user_settings = sdk.get_user_settings().await?; info!("User settings: {:?}", user_settings); ``` #### Swift ```swift let userSettings = try await sdk.getUserSettings() print("User settings: \(userSettings)") ``` #### Kotlin ```kotlin try { val userSettings = sdk.getUserSettings() println("User settings: $userSettings") } catch (e: Exception) { // handle error } ``` #### C# ```csharp var userSettings = await sdk.GetUserSettings(); Console.WriteLine($"User settings: {userSettings}"); ``` #### Javascript (Wasm) ```typescript const userSettings = await sdk.getUserSettings() console.log(`User settings: ${JSON.stringify(userSettings)}`) ``` #### React Native ```typescript const userSettings = await sdk.getUserSettings() console.log(`User settings: ${JSON.stringify(userSettings)}`) ``` #### Flutter ```dart final userSettings = await sdk.getUserSettings(); print('User settings: $userSettings'); ``` #### Python ```python try: user_settings = await sdk.get_user_settings() print(f"User settings: {user_settings}") except Exception as error: logging.error(error) raise ``` #### Go ```go userSettings, err := sdk.GetUserSettings() if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } log.Printf("User settings: %v", userSettings) ``` The `stable_balance_active_label` field will be unset if Stable Balance is deactivated, or the label of the currently active token. ## Sending payments with stable balance When your balance is held in a stablecoin, you can still send bitcoin payments. The SDK detects when there's not enough bitcoin balance to cover a payment and sets up the token-to-bitcoin conversion for you. When you [prepare to send a payment](/guide/send_payment.md#preparing-payments) without specifying conversion options: 1. If you have enough bitcoin balance, no conversion is needed 2. If your bitcoin balance is insufficient, the SDK configures conversion options using your Stable Balance settings (token identifier and slippage) For [bitcoin address](/guide/send_payment.md#bitcoin) destinations the prepare response marks its fee quote `is_estimate` when your bitcoin balance is zero. Any balance at all still gets a real quote. An estimate is always replaced with a real one before the payment goes out. The same flow extends to [USDC/USDT](/guide/send_payment.md#usdc-usdt): when paying a recipient on USDC or USDT, the SDK can spend the user's USDB balance directly (Orchestra routes that accept USDB as source) or auto-convert it through bitcoin if the chosen route only accepts sats. **Developer note** You can still explicitly specify `conversion options` in your request if you need custom slippage settings or want to override the default behavior. ## Sending entire balance When Stable Balance is active, you can send your entire balance, both the token balance and any remaining bitcoin, in a single payment. To send all, provide the full token balance as the amount along with `FeePolicy::FeesIncluded` and `ConversionType::ToBitcoin` conversion options. The SDK converts all specified tokens to bitcoin, combines the result with any existing bitcoin balance, and deducts payment fees from the total. The prepare response returns the estimated total Bitcoin available after conversion, and includes a `conversion_estimate` with the conversion details. The same approach works with `prepare_lnurl_pay` for [LNURL payments](/guide/lnurl_pay.md). ### Rust ```rust let payment_request = "".to_string(); let token_identifier = "".to_string(); let info = sdk .get_info(GetInfoRequest { ensure_synced: Some(false), }) .await?; let token_balance = info .token_balances .get(&token_identifier) .ok_or_else(|| anyhow::anyhow!("Token balance not found"))?; let conversion_options = Some(ConversionOptions { conversion_type: ConversionType::ToBitcoin { from_token_identifier: token_identifier.clone(), }, max_slippage_bps: None, completion_timeout_secs: None, }); let prepare_response = sdk .prepare_send_payment(PrepareSendPaymentRequest { payment_request: PaymentRequest::Input { input: payment_request, }, amount: Some(token_balance.balance), token_identifier: Some(token_identifier), conversion_options, fee_policy: Some(FeePolicy::FeesIncluded), }) .await?; // The response amount is the estimated total sats available // (converted sats + existing sat balance) info!("Total sats available: {}", prepare_response.amount); if let Some(conversion_estimate) = &prepare_response.conversion_estimate { info!( "Converting {} token units → ~{} sats", conversion_estimate.amount_in, conversion_estimate.amount_out ); info!("Conversion fee: {} token units", conversion_estimate.fee); } ``` ### Swift ```swift let paymentRequest = "" let tokenIdentifier = "" let info = try await sdk.getInfo( request: GetInfoRequest(ensureSynced: false)) guard let tokenBalance = info.tokenBalances[tokenIdentifier] else { throw SdkError.InvalidInput("Token balance not found") } let conversionOptions = ConversionOptions( conversionType: ConversionType.toBitcoin( fromTokenIdentifier: tokenIdentifier ), maxSlippageBps: nil, completionTimeoutSecs: nil ) let prepareResponse = try await sdk.prepareSendPayment( request: PrepareSendPaymentRequest( paymentRequest: .input(input: paymentRequest), amount: tokenBalance.balance, tokenIdentifier: tokenIdentifier, conversionOptions: conversionOptions, feePolicy: .feesIncluded )) // The response amount is the estimated total sats available // (converted sats + existing sat balance) print("Total sats available: \(prepareResponse.amount)") if let conversionEstimate = prepareResponse.conversionEstimate { print( "Converting \(conversionEstimate.amountIn) token units " + "→ ~\(conversionEstimate.amountOut) sats") print("Conversion fee: \(conversionEstimate.fee) token units") } ``` ### Kotlin ```kotlin val paymentRequest = "" val tokenIdentifier = "" try { val info = sdk.getInfo(GetInfoRequest(false)) val tokenBalance = info.tokenBalances[tokenIdentifier] ?: throw Exception("Token balance not found") val conversionOptions = ConversionOptions( conversionType = ConversionType.ToBitcoin( tokenIdentifier ), maxSlippageBps = null, completionTimeoutSecs = null ) val req = PrepareSendPaymentRequest( paymentRequest = PaymentRequest.Input(input = paymentRequest), amount = tokenBalance.balance, tokenIdentifier = tokenIdentifier, conversionOptions = conversionOptions, feePolicy = FeePolicy.FEES_INCLUDED, ) val prepareResponse = sdk.prepareSendPayment(req) // The response amount is the estimated total sats available // (converted sats + existing sat balance) // Log.v("Breez", "Total sats available: ${prepareResponse.amount}") prepareResponse.conversionEstimate?.let { conversionEstimate -> // Log.v("Breez", "Converting ${conversionEstimate.amountIn} token // units → ~${conversionEstimate.amountOut} sats") // Log.v("Breez", "Conversion fee: ${conversionEstimate.fee} token units") } } catch (e: Exception) { // handle error } ``` ### C# ```csharp var paymentRequest = ""; var tokenIdentifier = ""; var info = await sdk.GetInfo(request: new GetInfoRequest(ensureSynced: false)); if (!info.tokenBalances.TryGetValue(tokenIdentifier, out var tokenBalance)) { throw new Exception("Token balance not found"); } var conversionOptions = new ConversionOptions( conversionType: new ConversionType.ToBitcoin( fromTokenIdentifier: tokenIdentifier ), maxSlippageBps: null, completionTimeoutSecs: null ); var request = new PrepareSendPaymentRequest( paymentRequest: new PaymentRequest.Input(input: paymentRequest), amount: tokenBalance.balance, tokenIdentifier: tokenIdentifier, conversionOptions: conversionOptions, feePolicy: FeePolicy.FeesIncluded ); var prepareResponse = await sdk.PrepareSendPayment(request: request); // The response amount is the estimated total sats available // (converted sats + existing sat balance) Console.WriteLine($"Total sats available: {prepareResponse.amount}"); if (prepareResponse.conversionEstimate != null) { Console.WriteLine("Converting " + $"{prepareResponse.conversionEstimate.amountIn} token units " + $"→ ~{prepareResponse.conversionEstimate.amountOut} sats"); Console.WriteLine("Conversion fee: " + $"{prepareResponse.conversionEstimate.fee} token units"); } ``` ### Javascript (Wasm) ```typescript const paymentRequest = '' const tokenIdentifier = '' const info = await sdk.getInfo({ ensureSynced: false }) const tokenBalance = info.tokenBalances.get(tokenIdentifier) if (tokenBalance === undefined) { throw new Error('Token balance not found') } const conversionOptions: ConversionOptions = { conversionType: { type: 'toBitcoin', fromTokenIdentifier: tokenIdentifier } } const feePolicy: FeePolicy = 'feesIncluded' const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: { type: 'input', input: paymentRequest }, amount: tokenBalance.balance, tokenIdentifier, conversionOptions, feePolicy }) // The response amount is the estimated total sats available // (converted sats + existing sat balance) console.log(`Total sats available: ${prepareResponse.amount}`) if (prepareResponse.conversionEstimate !== undefined) { const estimate = prepareResponse.conversionEstimate console.log(`Converting ${estimate.amountIn} token units → ~${estimate.amountOut} sats`) console.log(`Conversion fee: ${estimate.fee} token units`) } ``` ### React Native ```typescript const paymentRequest = '' const tokenIdentifier = '' const info = await sdk.getInfo({ ensureSynced: false }) const tokenBalance = info.tokenBalances.get(tokenIdentifier) if (tokenBalance === undefined) { throw new Error('Token balance not found') } const conversionOptions = { conversionType: new ConversionType.ToBitcoin({ fromTokenIdentifier: tokenIdentifier }), maxSlippageBps: undefined, completionTimeoutSecs: undefined } const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: new PaymentRequest.Input({ input: paymentRequest }), amount: tokenBalance.balance, tokenIdentifier, conversionOptions, feePolicy: FeePolicy.FeesIncluded }) // The response amount is the estimated total sats available // (converted sats + existing sat balance) console.log(`Total sats available: ${prepareResponse.amount}`) if (prepareResponse.conversionEstimate !== undefined) { const estimate = prepareResponse.conversionEstimate console.log(`Converting ${estimate.amountIn} token units → ~${estimate.amountOut} sats`) console.log(`Conversion fee: ${estimate.fee} token units`) } ``` ### Flutter ```dart String paymentRequest = ""; String tokenIdentifier = ""; final info = await sdk.getInfo(request: GetInfoRequest(ensureSynced: false)); final tokenBalance = info.tokenBalances[tokenIdentifier]; if (tokenBalance == null) { throw Exception("Token balance not found"); } final conversionOptions = ConversionOptions( conversionType: ConversionType.toBitcoin( fromTokenIdentifier: tokenIdentifier, ), ); final request = PrepareSendPaymentRequest( paymentRequest: PaymentRequest.input(input: paymentRequest), amount: tokenBalance.balance, tokenIdentifier: tokenIdentifier, conversionOptions: conversionOptions, feePolicy: FeePolicy.feesIncluded); final response = await sdk.prepareSendPayment(request: request); // The response amount is the estimated total sats available // (converted sats + existing sat balance) print("Total sats available: ${response.amount}"); if (response.conversionEstimate != null) { print( "Converting ${response.conversionEstimate!.amountIn} token units " "→ ~${response.conversionEstimate!.amountOut} sats"); print( "Conversion fee: ${response.conversionEstimate!.fee} token units"); } ``` ### Python ```python payment_request = "" token_identifier = "" try: info = await sdk.get_info(request=GetInfoRequest(ensure_synced=False)) token_balance = info.token_balances.get(token_identifier) if token_balance is None: raise ValueError("Token balance not found") conversion_options = ConversionOptions( conversion_type=ConversionType.TO_BITCOIN( from_token_identifier=token_identifier ), ) request = PrepareSendPaymentRequest( payment_request=PaymentRequest.INPUT(input=payment_request), amount=token_balance.balance, token_identifier=token_identifier, conversion_options=conversion_options, fee_policy=FeePolicy.FEES_INCLUDED, ) prepare_response = await sdk.prepare_send_payment(request=request) # The response amount is the estimated total sats available # (converted sats + existing sat balance) logging.debug(f"Total sats available: {prepare_response.amount}") if prepare_response.conversion_estimate is not None: conversion_estimate = prepare_response.conversion_estimate logging.debug( f"Converting {conversion_estimate.amount_in}" f" token units → ~{conversion_estimate.amount_out} sats" ) logging.debug( f"Conversion fee: {conversion_estimate.fee} token units" ) except Exception as error: logging.error(error) raise ``` ### Go ```go paymentRequest := "" tokenIdentifier := "" ensureSynced := false info, err := sdk.GetInfo(breez_sdk_spark.GetInfoRequest{ EnsureSynced: &ensureSynced, }) if err != nil { return nil, err } tokenBalance, ok := info.TokenBalances[tokenIdentifier] if !ok { return nil, errors.New("token balance not found") } conversionOptions := breez_sdk_spark.ConversionOptions{ ConversionType: breez_sdk_spark.ConversionTypeToBitcoin{ FromTokenIdentifier: tokenIdentifier, }, } feePolicy := breez_sdk_spark.FeePolicyFeesIncluded request := breez_sdk_spark.PrepareSendPaymentRequest{ PaymentRequest: breez_sdk_spark.PaymentRequestInput{Input: paymentRequest}, Amount: &tokenBalance.Balance, TokenIdentifier: &tokenIdentifier, ConversionOptions: &conversionOptions, FeePolicy: &feePolicy, } response, err := sdk.PrepareSendPayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } // The response amount is the estimated total sats available // (converted sats + existing sat balance) log.Printf("Total sats available: %v", response.Amount) if response.ConversionEstimate != nil { log.Printf( "Converting %v token units → ~%v sats", response.ConversionEstimate.AmountIn, response.ConversionEstimate.AmountOut, ) log.Printf("Conversion fee: %v token units", response.ConversionEstimate.Fee) } ``` **Developer note** The actual sats received from conversion may differ slightly from the estimate due to price movement. The SDK handles this by querying the actual balance after conversion completes and sending the full available amount. ## Conversion details Payments involving token conversions include a `conversion_details` field that describes the conversion that took place. This is useful for displaying conversion context in your UI. ### Status The `status` field tracks the lifecycle of the conversion: | Status | Description | |--------|-------------| | `ConversionStatus::Pending` | Conversion is queued or in progress | | `ConversionStatus::Completed` | Conversion finished successfully | | `ConversionStatus::Failed` | Conversion could not be completed | | `ConversionStatus::RefundNeeded` | Conversion failed and requires a refund | | `ConversionStatus::Refunded` | Failed conversion has been refunded | ### Conversion steps The `from` and `to` fields are conversion step objects describing each side of the conversion: | Field | Description | |-------|-------------| | `payment_id` | The ID of the internal conversion payment | | `amount` | The amount in the step's denomination (sats or token units) | | `fee` | Fee charged for this step | | `method` | Payment method (`PaymentMethod::Spark` for bitcoin, `PaymentMethod::Token` for stablecoins) | | `token_metadata` | Token metadata (name, symbol, etc.) — present when method is `PaymentMethod::Token` | | `amount_adjustment` | Present if the amount was modified before conversion (see [amount adjustments](#amount-adjustments)) | ### Amount adjustments The `amount_adjustment` field is present when the conversion amount was modified before execution: | Reason | Description | |--------|-------------| | `AmountAdjustmentReason::FlooredToMinLimit` | Amount was increased to meet the minimum conversion limit | | `AmountAdjustmentReason::IncreasedToAvoidDust` | Amount was increased to convert the entire remaining balance, avoiding a leftover too small to convert back | ## Related pages - [Token conversion](/guide/token_conversion.md) - Learn about converting between Bitcoin and tokens - [Custom configuration](/guide/config.md#stable-balance-configuration) - All configuration options - [User settings](/guide/user_settings.md) - Getting and updating user settings - [Handling tokens](/guide/tokens.md) - Working with tokens in the SDK --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [User settings](/guide/user_settings.md)** # User settings The SDK exposes a set of user settings that are shared across all SDK instances, even from different partners. ## Available user settings API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.UserSettings.html The following user settings are available: - **Spark private mode**: Spark supports opt-in wallet privacy. When enabled, the wallet's Bitcoin payments and balance will not be accessible through public indexers like [Sparkscan](https://sparkscan.io). The SDK enables this by default for new wallets, and we highly recommend keeping it enabled. However, some applications may require the wallet to be visible to the public. > **Note:** Spark private mode only applies to Bitcoin payments. Token payments are not affected by the private mode and will still be publicly available. - **Stable balance active label**: Controls which stable token is active for automatic Bitcoin-to-token conversion. Set to a label from your [stable balance configuration](/guide/config.md#stable-balance-configuration) to activate, or unset to deactivate. See the [Stable balance](/guide/stable_balance.md) guide for details. - **Spark master identity public key**: A second public key that Spark accepts as a reader of the wallet while private mode is enabled. It enables watch-only views of a private wallet: designate a key you control, and a Spark client authenticating with the corresponding private key can query the wallet's Bitcoin balance and payment history. The master identity is read-only, so making payments still requires the wallet's own keys. The same public key can be designated across many wallets. ## Getting the current user settings API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.get_user_settings ### Rust ```rust let user_settings = sdk.get_user_settings().await?; info!("User settings: {:?}", user_settings); ``` ### Swift ```swift let userSettings = try await sdk.getUserSettings() print("User settings: \(userSettings)") ``` ### Kotlin ```kotlin try { val userSettings = sdk.getUserSettings() println("User settings: $userSettings") } catch (e: Exception) { // handle error } ``` ### C# ```csharp var userSettings = await sdk.GetUserSettings(); Console.WriteLine($"User settings: {userSettings}"); ``` ### Javascript (Wasm) ```typescript const userSettings = await sdk.getUserSettings() console.log(`User settings: ${JSON.stringify(userSettings)}`) ``` ### React Native ```typescript const userSettings = await sdk.getUserSettings() console.log(`User settings: ${JSON.stringify(userSettings)}`) ``` ### Flutter ```dart final userSettings = await sdk.getUserSettings(); print('User settings: $userSettings'); ``` ### Python ```python try: user_settings = await sdk.get_user_settings() print(f"User settings: {user_settings}") except Exception as error: logging.error(error) raise ``` ### Go ```go userSettings, err := sdk.GetUserSettings() if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } log.Printf("User settings: %v", userSettings) ``` ## Updating the user settings API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.update_user_settings Every field of `UpdateUserSettingsRequest` is optional, and a field left unset is not changed. Settings that hold a value use an enum to distinguish assigning one from clearing it: `SparkMasterIdentityPublicKey::Set` and `StableBalanceActiveLabel::Set` assign, `SparkMasterIdentityPublicKey::Unset` and `StableBalanceActiveLabel::Unset` clear. ### Rust ```rust sdk.update_user_settings(UpdateUserSettingsRequest { spark_private_mode_enabled: Some(true), stable_balance_active_label: None, spark_master_identity_public_key: Some(SparkMasterIdentityPublicKey::Set { public_key: "".to_string(), }), }) .await?; ``` ### Swift ```swift try await sdk.updateUserSettings( request: UpdateUserSettingsRequest( sparkPrivateModeEnabled: true, stableBalanceActiveLabel: nil, sparkMasterIdentityPublicKey: .set(publicKey: "") )) ``` ### Kotlin ```kotlin try { sdk.updateUserSettings(UpdateUserSettingsRequest( sparkPrivateModeEnabled = true, stableBalanceActiveLabel = null, sparkMasterIdentityPublicKey = SparkMasterIdentityPublicKey.Set( publicKey = "" ) )) } catch (e: Exception) { // handle error } ``` ### C# ```csharp await sdk.UpdateUserSettings( request: new UpdateUserSettingsRequest( sparkPrivateModeEnabled: true, stableBalanceActiveLabel: null, sparkMasterIdentityPublicKey: new SparkMasterIdentityPublicKey.Set( publicKey: "" ) ) ); ``` ### Javascript (Wasm) ```typescript await sdk.updateUserSettings({ sparkPrivateModeEnabled: true, stableBalanceActiveLabel: undefined, sparkMasterIdentityPublicKey: { type: 'set', publicKey: '' } }) ``` ### React Native ```typescript await sdk.updateUserSettings({ sparkPrivateModeEnabled: true, stableBalanceActiveLabel: undefined, sparkMasterIdentityPublicKey: new SparkMasterIdentityPublicKey.Set({ publicKey: '' }) }) ``` ### Flutter ```dart await sdk.updateUserSettings( request: UpdateUserSettingsRequest( sparkPrivateModeEnabled: true, stableBalanceActiveLabel: null, sparkMasterIdentityPublicKey: SparkMasterIdentityPublicKey_Set( publicKey: ""))); ``` ### Python ```python try: await sdk.update_user_settings( request=UpdateUserSettingsRequest( spark_private_mode_enabled=True, stable_balance_active_label=None, spark_master_identity_public_key=SparkMasterIdentityPublicKey.SET( public_key="" ) ) ) except Exception as error: logging.error(error) raise ``` ### Go ```go sparkPrivateModeEnabled := true masterIdentityPublicKey := breez_sdk_spark.SparkMasterIdentityPublicKey( breez_sdk_spark.SparkMasterIdentityPublicKeySet{ PublicKey: "", }, ) err := sdk.UpdateUserSettings(breez_sdk_spark.UpdateUserSettingsRequest{ SparkPrivateModeEnabled: &sparkPrivateModeEnabled, StableBalanceActiveLabel: nil, SparkMasterIdentityPublicKey: &masterIdentityPublicKey, }) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } ``` --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Signing and verifying messages](/guide/messages.md)** # Signing and verifying messages Through signing and verifying messages we can provide proof that a digital signature was created by a private key. ## Signing a message API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.sign_message By signing a message using the SDK we can provide a digital signature. Anyone with the `message`, `pubkey` and `signature` can verify the signature was created by the private key of this pubkey. Messages starting with `breez-lnurl:` are refused: that namespace is reserved for requests to the Lightning address server. ### Rust ```rust let message = "".to_string(); // Set to true to get a compact signature rather than a DER let compact = true; let sign_message_request = SignMessageRequest { message, compact }; let sign_message_response = sdk.sign_message(sign_message_request).await?; let signature = sign_message_response.signature; let pubkey = sign_message_response.pubkey; info!("Pubkey: {}", pubkey); info!("Signature: {}", signature); ``` ### Swift ```swift // Set to true to get a compact signature rather than a DER let compact = true let signMessageRequest = SignMessageRequest( message: "", compact: compact ) let signMessageResponse = try await sdk .signMessage(request: signMessageRequest) let signature = signMessageResponse.signature let pubkey = signMessageResponse.pubkey print("Pubkey: {}", pubkey); print("Signature: {}", signature); ``` ### Kotlin ```kotlin val message = "" // Set to true to get a compact signature rather than a DER val compact = true try { val signMessageRequest = SignMessageRequest(message, compact) val signMessageResponse = sdk.signMessage(signMessageRequest) val signature = signMessageResponse?.signature val pubkey = signMessageResponse?.pubkey // Log.v("Breez", "Pubkey: ${pubkey}") // Log.v("Breez", "Signature: ${signature}") } catch (e: Exception) { // handle error } ``` ### C# ```csharp var message = ""; // Set to true to get a compact signature rather than a DER var compact = true; var signMessageRequest = new SignMessageRequest( message: message, compact: compact ); var signMessageResponse = await sdk.SignMessage(request: signMessageRequest); var signature = signMessageResponse.signature; var pubkey = signMessageResponse.pubkey; Console.WriteLine($"Pubkey: {pubkey}"); Console.WriteLine($"Signature: {signature}"); ``` ### Javascript (Wasm) ```typescript // Set to true to get a compact signature rather than a DER const compact = true const signMessageResponse = await sdk.signMessage({ message: '', compact }) const signature = signMessageResponse.signature const pubkey = signMessageResponse.pubkey console.log(`Pubkey: ${pubkey}`) console.log(`Signature: ${signature}`) ``` ### React Native ```typescript // Set to true to get a compact signature rather than a DER const compact = true const signMessageResponse = await sdk.signMessage({ message: '', compact }) const signature = signMessageResponse.signature const pubkey = signMessageResponse.pubkey console.log(`Pubkey: ${pubkey}`) console.log(`Signature: ${signature}`) ``` ### Flutter ```dart // Set to true to get a compact signature rather than a DER bool compact = true; SignMessageRequest signMessageRequest = SignMessageRequest( message: "", compact: compact, ); SignMessageResponse signMessageResponse = await sdk.signMessage( request: signMessageRequest, ); String signature = signMessageResponse.signature; String pubkey = signMessageResponse.pubkey; print("Pubkey: $pubkey"); print("Signature: $signature"); ``` ### Python ```python message = "" # Set to true to get a compact signature rather than a DER compact = True try: sign_message_request = SignMessageRequest( message=message, compact=compact ) sign_message_response = await sdk.sign_message(request=sign_message_request) signature = sign_message_response.signature pubkey = sign_message_response.pubkey logging.debug(f"Pubkey: {pubkey}") logging.debug(f"Signature: {signature}") except Exception as error: logging.error(error) raise ``` ### Go ```go message := "" // Set to true to get a compact signature rather than a DER compact := true signMessageRequest := breez_sdk_spark.SignMessageRequest{ Message: message, Compact: compact, } signMessageResponse, err := sdk.SignMessage(signMessageRequest) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } signature := signMessageResponse.Signature pubkey := signMessageResponse.Pubkey log.Printf("Pubkey: %v", pubkey) log.Printf("Signature: %v", signature) ``` ## Verifying a message API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.check_message You can prove control of a private key by verifying a `message` with it's `signature` and `pubkey`. ### Rust ```rust let check_message_request = CheckMessageRequest { message: "".to_string(), pubkey: "".to_string(), signature: "".to_string(), }; let check_message_response = sdk.check_message(check_message_request).await?; let is_valid = check_message_response.is_valid; info!("Signature valid: {}", is_valid); ``` ### Swift ```swift let checkMessageRequest = CheckMessageRequest( message: "", pubkey: "", signature: "" ) let checkMessageResponse = try await sdk .checkMessage(request: checkMessageRequest) let isValid = checkMessageResponse.isValid print("Signature valid: {}", isValid); ``` ### Kotlin ```kotlin val message = "" val pubkey = "" val signature = "" try { val checkMessageRequest = CheckMessageRequest(message, pubkey, signature) val checkMessageResponse = sdk.checkMessage(checkMessageRequest) val isValid = checkMessageResponse?.isValid // Log.v("Breez", "Signature valid: ${isValid}") } catch (e: Exception) { // handle error } ``` ### C# ```csharp var message = ""; var pubkey = ""; var signature = ""; var checkMessageRequest = new CheckMessageRequest( message: message, pubkey: pubkey, signature: signature ); var checkMessageResponse = await sdk.CheckMessage(request: checkMessageRequest); var isValid = checkMessageResponse.isValid; Console.WriteLine($"Signature valid: {isValid}"); ``` ### Javascript (Wasm) ```typescript const checkMessageResponse = await sdk.checkMessage({ message: '', pubkey: '', signature: '' }) const isValid = checkMessageResponse.isValid console.log(`Signature valid: ${isValid}`) ``` ### React Native ```typescript const checkMessageResponse = await sdk.checkMessage({ message: '', pubkey: '', signature: '' }) const isValid = checkMessageResponse.isValid console.log(`Signature valid: ${isValid}`) ``` ### Flutter ```dart CheckMessageRequest checkMessageRequest = CheckMessageRequest( message: "", pubkey: "", signature: "", ); CheckMessageResponse checkMessageResponse = await sdk.checkMessage( request: checkMessageRequest, ); bool isValid = checkMessageResponse.isValid; print("Signature valid: $isValid"); ``` ### Python ```python message = "" pubkey = "" signature = "" try: check_message_request = CheckMessageRequest( message=message, pubkey=pubkey, signature=signature ) check_message_response = await sdk.check_message(request=check_message_request) is_valid = check_message_response.is_valid logging.debug(f"Signature valid: {is_valid}") except Exception as error: logging.error(error) raise ``` ### Go ```go message := "" pubkey := "" signature := "" checkMessageRequest := breez_sdk_spark.CheckMessageRequest{ Message: message, Pubkey: pubkey, Signature: signature, } checkMessageResponse, err := sdk.CheckMessage(checkMessageRequest) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } isValid := checkMessageResponse.IsValid log.Printf("Signature valid: %v", isValid) ``` **→ [Supporting fiat currencies](/guide/fiat_currencies.md)** # Supporting fiat currencies ## List fiat currencies API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.list_fiat_currencies You can get the full details of supported fiat currencies, such as symbols and localized names: ### Rust ```rust let response = sdk.list_fiat_currencies().await?; ``` ### Swift ```swift let response = try await sdk.listFiatCurrencies() ``` ### Kotlin ```kotlin try { val response = sdk.listFiatCurrencies() } catch (e: Exception) { // handle error } ``` ### C# ```csharp var response = await sdk.ListFiatCurrencies(); ``` ### Javascript (Wasm) ```typescript const response = await sdk.listFiatCurrencies() ``` ### React Native ```typescript const response = await sdk.listFiatCurrencies() ``` ### Flutter ```dart ListFiatCurrenciesResponse response = await sdk.listFiatCurrencies(); ``` ### Python ```python try: response = await sdk.list_fiat_currencies() except Exception as error: print(error) raise ``` ### Go ```go response, err := sdk.ListFiatCurrencies() if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } ``` ## Fetch fiat rates API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.list_fiat_rates To get the current BTC rate in the various supported fiat currencies: ### Rust ```rust let response = sdk.list_fiat_rates().await?; ``` ### Swift ```swift let response = try await sdk.listFiatRates() ``` ### Kotlin ```kotlin try { val response = sdk.listFiatRates() } catch (e: Exception) { // handle error } ``` ### C# ```csharp var response = await sdk.ListFiatRates(); ``` ### Javascript (Wasm) ```typescript const response = await sdk.listFiatRates() ``` ### React Native ```typescript const response = await sdk.listFiatRates() ``` ### Flutter ```dart ListFiatRatesResponse response = await sdk.listFiatRates(); ``` ### Python ```python try: response = await sdk.list_fiat_rates() except Exception as error: print(error) raise ``` ### Go ```go response, err := sdk.ListFiatRates() if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } ``` **→ [Buying Bitcoin](/guide/buy_bitcoin.md)** # Buying Bitcoin The Breez SDK allows users to purchase Bitcoin through external providers. Two providers are currently supported: **MoonPay** and **CashApp**. The user is directed to a provider URL in their browser (or the CashApp app) to complete the purchase, and the funds are deposited directly into their wallet. ## MoonPay MoonPay uses **on-chain Bitcoin deposit addresses** to receive purchased funds. It supports fiat-to-Bitcoin purchases via credit card, Apple Pay, Google Pay, and other payment methods. To initiate a Bitcoin purchase via MoonPay: ### Rust ```rust // Optionally, lock the purchase to a specific amount let optional_locked_amount_sat = Some(100_000); // Optionally, set a redirect URL for after the purchase is completed let optional_redirect_url = Some("https://example.com/purchase-complete".to_string()); let request = BuyBitcoinRequest::Moonpay { locked_amount_sat: optional_locked_amount_sat, redirect_url: optional_redirect_url, }; let response = sdk.buy_bitcoin(request).await?; info!("Open this URL in a browser to complete the purchase:"); info!("{}", response.url); ``` ### Swift ```swift // Optionally, lock the purchase to a specific amount let optionalLockedAmountSat: UInt64 = 100_000 // Optionally, set a redirect URL for after the purchase is completed let optionalRedirectUrl = "https://example.com/purchase-complete" let request = BuyBitcoinRequest.moonpay( lockedAmountSat: optionalLockedAmountSat, redirectUrl: optionalRedirectUrl ) let response = try await sdk.buyBitcoin(request: request) print("Open this URL in a browser to complete the purchase:") print("\(response.url)") ``` ### Kotlin ```kotlin // Optionally, lock the purchase to a specific amount val optionalLockedAmountSat: ULong = 100_000u // Optionally, set a redirect URL for after the purchase is completed val optionalRedirectUrl = "https://example.com/purchase-complete" val request = BuyBitcoinRequest.Moonpay( lockedAmountSat = optionalLockedAmountSat, redirectUrl = optionalRedirectUrl ) val response = sdk.buyBitcoin(request) // Log.v("Breez", "Open this URL in a browser to complete the purchase:") // Log.v("Breez", "${response.url}") ``` ### C# ```csharp // Optionally, lock the purchase to a specific amount var optionalLockedAmountSat = (ulong)100000; // Optionally, set a redirect URL for after the purchase is completed var optionalRedirectUrl = "https://example.com/purchase-complete"; var request = new BuyBitcoinRequest.Moonpay( lockedAmountSat: optionalLockedAmountSat, redirectUrl: optionalRedirectUrl ); var response = await sdk.BuyBitcoin(request: request); Console.WriteLine("Open this URL in a browser to complete the purchase:"); Console.WriteLine($"{response.url}"); ``` ### Javascript (Wasm) ```typescript // Optionally, lock the purchase to a specific amount const optionalLockedAmountSat = 100_000 // Optionally, set a redirect URL for after the purchase is completed const optionalRedirectUrl = 'https://example.com/purchase-complete' const response = await sdk.buyBitcoin({ type: 'moonpay', lockedAmountSat: optionalLockedAmountSat, redirectUrl: optionalRedirectUrl }) console.log('Open this URL in a browser to complete the purchase:') console.log(response.url) ``` ### React Native ```typescript // Optionally, lock the purchase to a specific amount const optionalLockedAmountSat = BigInt(100_000) // Optionally, set a redirect URL for after the purchase is completed const optionalRedirectUrl = 'https://example.com/purchase-complete' const request = new BuyBitcoinRequest.Moonpay({ lockedAmountSat: optionalLockedAmountSat, redirectUrl: optionalRedirectUrl }) const response = await sdk.buyBitcoin(request) console.log('Open this URL in a browser to complete the purchase:') console.log(response.url) ``` ### Flutter ```dart // Optionally, lock the purchase to a specific amount final optionalLockedAmountSat = BigInt.from(100000); // Optionally, set a redirect URL for after the purchase is completed final optionalRedirectUrl = "https://example.com/purchase-complete"; final request = BuyBitcoinRequest_Moonpay( lockedAmountSat: optionalLockedAmountSat, redirectUrl: optionalRedirectUrl); final response = await sdk.buyBitcoin(request: request); print("Open this URL in a browser to complete the purchase:"); print(response.url); ``` ### Python ```python # Optionally, lock the purchase to a specific amount optional_locked_amount_sat = 100_000 # Optionally, set a redirect URL for after the purchase is completed optional_redirect_url = "https://example.com/purchase-complete" try: request = BuyBitcoinRequest.MOONPAY( locked_amount_sat=optional_locked_amount_sat, redirect_url=optional_redirect_url, ) response = await sdk.buy_bitcoin(request=request) logging.debug("Open this URL in a browser to complete the purchase:") logging.debug(response.url) except Exception as error: logging.error(error) raise ``` ### Go ```go optionalLockedAmountSat := uint64(100_000) optionalRedirectUrl := "https://example.com/purchase-complete" request := breez_sdk_spark.BuyBitcoinRequestMoonpay{ LockedAmountSat: &optionalLockedAmountSat, RedirectUrl: &optionalRedirectUrl, } response, err := sdk.BuyBitcoin(request) if err != nil { return err } log.Printf("Open this URL in a browser to complete the purchase:") log.Printf("%v", response.Url) ``` The returned URL should be opened in a browser for the user to complete the purchase. **Developer note** MoonPay supports Apple Pay and Google Pay, but these payment methods will not work inside an iframe or standard web view. To ensure compatibility: - **iOS**: Open the URL using SFSafariViewController. - **Android**: Open the URL using Chrome Custom Tabs. - **Desktop**: Apple Pay requires Safari; Google Pay requires Chrome. ## CashApp CashApp uses **Lightning (bolt11 invoices)** to receive purchased funds. The caller specifies the amount in satoshis; the SDK generates a bolt11 invoice for that amount and returns a CashApp deep link (`cash.app/launch/lightning/...`) that opens CashApp so the user can complete payment. **Developer notes**
  • CashApp is only available on mainnet. Using CashApp on testnet or regtest returns an error.
  • The amount is required. With an amountless invoice, Cash App only lets the payer fund from their existing Cash App BTC balance. When the invoice carries an amount, Cash App opens up funding via fiat balance and debit card.
To initiate a Bitcoin purchase via CashApp: ### Rust ```rust // Cash App requires the amount to be specified up front. let amount_sats = 50_000; let request = BuyBitcoinRequest::CashApp { amount_sats }; let response = sdk.buy_bitcoin(request).await?; info!("Open this URL in Cash App to complete the purchase:"); info!("{}", response.url); ``` ### Swift ```swift // Cash App requires the amount to be specified up front. let amountSats: UInt64 = 50_000 let request = BuyBitcoinRequest.cashApp(amountSats: amountSats) let response = try await sdk.buyBitcoin(request: request) print("Open this URL in Cash App to complete the purchase:") print("\(response.url)") ``` ### Kotlin ```kotlin // Cash App requires the amount to be specified up front. val amountSats: ULong = 50_000u val request = BuyBitcoinRequest.CashApp(amountSats = amountSats) val response = sdk.buyBitcoin(request) // Log.v("Breez", "Open this URL in Cash App to complete the purchase:") // Log.v("Breez", "${response.url}") ``` ### C# ```csharp // Cash App requires the amount to be specified up front. var amountSats = (ulong)50_000; var request = new BuyBitcoinRequest.CashApp( amountSats: amountSats ); var response = await sdk.BuyBitcoin(request: request); Console.WriteLine("Open this URL in Cash App to complete the purchase:"); Console.WriteLine($"{response.url}"); ``` ### Javascript (Wasm) ```typescript // Cash App requires the amount to be specified up front. const amountSats = 50_000 const response = await sdk.buyBitcoin({ type: 'cashApp', amountSats }) console.log('Open this URL in Cash App to complete the purchase:') console.log(response.url) ``` ### React Native ```typescript // Cash App requires the amount to be specified up front. const amountSats = BigInt(50_000) const request = new BuyBitcoinRequest.CashApp({ amountSats }) const response = await sdk.buyBitcoin(request) console.log('Open this URL in Cash App to complete the purchase:') console.log(response.url) ``` ### Flutter ```dart // Cash App requires the amount to be specified up front. final amountSats = BigInt.from(50000); final request = BuyBitcoinRequest_CashApp(amountSats: amountSats); final response = await sdk.buyBitcoin(request: request); print("Open this URL in Cash App to complete the purchase:"); print(response.url); ``` ### Python ```python # Cash App requires the amount to be specified up front. amount_sats = 50_000 try: request = BuyBitcoinRequest.CASH_APP( amount_sats=amount_sats, ) response = await sdk.buy_bitcoin(request=request) logging.debug("Open this URL in Cash App to complete the purchase:") logging.debug(response.url) except Exception as error: logging.error(error) raise ``` ### Go ```go // Cash App requires the amount to be specified up front. amountSats := uint64(50_000) request := breez_sdk_spark.BuyBitcoinRequestCashApp{ AmountSats: amountSats, } response, err := sdk.BuyBitcoin(request) if err != nil { return err } log.Printf("Open this URL in Cash App to complete the purchase:") log.Printf("%v", response.Url) ``` The returned URL is a CashApp universal link (`https://cash.app/launch/lightning/`). On devices with CashApp installed it opens the app directly; otherwise it falls back to the CashApp website. ### Recommended UX 1. Collect a non-zero amount before calling `buy_bitcoin`. 2. On mobile, redirect to the returned URL. On desktop, render it as a QR code and dismiss when `SdkEvent::PaymentSucceeded` fires for the invoice. ### Popup blockers on the web On web, `window.open()` called after `await sdk.buyBitcoin(...)` is typically blocked by mobile browsers and PWAs because it falls outside the original user gesture. Pre-open a blank tab synchronously inside the click handler, then navigate it once the URL is ready: ```javascript // Open a placeholder tab during the user gesture so the browser // allows it; we navigate it once the SDK returns. const newTab = window.open('', '_blank'); // Generate the Cash App invoice for the chosen amount. const response = await sdk.buyBitcoin({ type: 'cashApp', amountSats: 50_000 }); // Send the user to Cash App. If the placeholder was blocked, redirect // the current tab. The OS opens Cash App via the universal link. if (newTab) { newTab.location.href = response.url; } else { window.location.href = response.url; } ``` ### Platform-specific guidance | Platform | Behavior | Recommendation | |----------|----------|----------------| | **Desktop browsers** | Pre-opened tab works reliably; most desktops won't have CashApp installed | Render the CashApp URL as a QR for the user to scan on their phone | | **Mobile browsers** | `window.open` may be blocked after async | Pre-open a tab (see above); falls back to `location.href` automatically | | **PWA (standalone)** | `window.open` is almost always blocked | Same-tab redirect via `location.href`; opens system browser, which hands off to CashApp | | **iOS (native)** | Universal link triggers CashApp if installed | Open via `UIApplication.open()` or `SFSafariViewController` | | **Android (native)** | Universal link triggers CashApp if installed | Open via `Intent` or Chrome Custom Tabs | **CashApp availability:** US and UK only (excluding New York State for Bitcoin/Lightning features). CashApp handles region restrictions on their end, so no client-side gating is needed. --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Send and receive USDC/USDT](/guide/cross_chain.md)** # Send and receive USDC/USDT The SDK can send and receive USDC and USDT between a Spark wallet and several supported external chains: Ethereum-family chains (Arbitrum, Base, and similar EVM networks), Solana, and Tron. The Spark side is either BTC sats or [USDB](/guide/stable_balance.md), the 6-decimal USD-pegged token on Spark. Each payment runs as two legs (a Spark-side transfer and the provider-driven external delivery) reconciled onto a single `Payment` row. The send flow is documented on the [Sending payments](/guide/send_payment.md#usdc-usdt) page; the receive flow on the [Receiving payments](/guide/receive_payment.md#usdc-usdt) page. This page covers shared concepts: providers, lifecycle, retry safety, and limitations. ## Supported address formats `parse` recognizes cross-chain destinations in the following forms, returning `InputType::CrossChainAddress` with the parsed `CrossChainAddressDetails` — address family, bare address, and optional token contract address, chain id, and amount. Parsing is for send only. Receives don't take a counterparty address: the receiver chooses a route and the SDK returns a provider-controlled deposit address for the sender to pay. ### Bare addresses The SDK detects three address families from format alone. A bare address parses with no `contract_address`, `chain_id`, or `amount` — the caller selects the destination chain and asset via `get_cross_chain_routes`. - **EVM** — `0x` + 40 hex characters (lowercase or checksummed): ``` 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 ``` - **Solana** — base58 encoding of a 32-byte public key: ``` EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v ``` - **Tron** — base58check with a `T` prefix (34 characters total): ``` TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t ``` ### Canonical URIs URIs let the recipient encode chain, token contract, and amount alongside the address. Unknown query parameters are ignored. - **EVM** — [EIP-681](https://eips.ethereum.org/EIPS/eip-681). Native send or ERC-20 transfer; the optional `@` suffix is the EIP-681 chain identifier (e.g. `8453` for Base): ``` ethereum:[@]?value= ethereum:[@]/transfer?address=&uint256= ``` - **Solana** — Solana Pay-style. `spl-token=` carries the SPL mint when the destination is an SPL token rather than native SOL: ``` solana:?amount=&spl-token= ``` - **Tron** — TRC-20 destinations carry the contract on `token=`: ``` tron:?amount=&token= ``` URIs whose recipient address doesn't match the scheme's address family (e.g. a `solana:` URI carrying an EVM address) are not recognized as cross-chain. Unknown schemes are not recognized as cross-chain either — they may still be classified by another input type if the format matches. ## Providers `get_cross_chain_routes` returns the routes offered by each provider, tagged with `CrossChainRoutePair.provider`. | Provider | Direction | Spark side | External side | Mechanism | | ------------ | ---------------- | ---------------- | ------------------------------------------------------------ | ------------------------------------ | | **Orchestra** (Flashnet) | Send + Receive | BTC sats + USDB | USDC / USDT on Ethereum chains (Arbitrum, Base), Solana, Tron | Spark transfer to a deposit address, then provider bridges to the destination chain | The provider tag on each `CrossChainRoutePair` is the source of truth. When the same destination is offered by multiple providers, every route is returned; the caller picks one based on supported source/destination assets, fees, or other preferences. ## Slippage Cross-chain slippage protects against price movement between quote and delivery. Values are expressed in basis points (1 bps = 0.01%). Resolution at prepare/receive time: 1. The per-request `max_slippage_bps` on the request if set. 2. Otherwise, the SDK falls back to `default_slippage_bps` on `CrossChainConfig` from [the SDK configuration](/guide/config.md#usdc-usdt). 3. Otherwise, the built-in default of 100 bps (1%) is used. Values outside 10 to 500 are rejected at both config validation and per-request validation. ## Target overpay On `CrossChainFeeMode::FeesExcluded` sends and receives the SDK pads the user's target amount by `target_overpay_bps` so the realized delivery lands at or above target despite provider slippage. Resolution at prepare/receive time: 1. The per-request `target_overpay_bps` on the request if set. 2. Otherwise, the SDK falls back to `default_target_overpay_bps` on `CrossChainConfig`. 3. Otherwise, the built-in default of 15 bps is used. Values outside 0 to 500 are rejected at both config validation and per-request validation. `CrossChainFeeMode::FeesIncluded` requests ignore this field. ## Status lifecycle The Spark-side transfer and the external cross-chain leg have distinct status fields. They are tracked separately on the persisted `Payment` row so each can settle independently. | Field | Reflects | | ----------------------------------------------------- | --------------------------------------------------------------------------- | | `status` | The Spark-side transfer (outbound on send; inbound claim on receive) | | `conversion_info.status` | The provider-driven cross-chain leg | | `conversion_info.delivered_amount` | Final amount delivered to the recipient, set when terminal | | `conversion_info.external_tx_hash` | Transaction on the non-Spark chain: the delivery, or the funding deposit | The cross-chain status walks one of: - **`ConversionStatus::Pending`**: deposit/transfer submitted, provider working on the cross-chain leg. - **`ConversionStatus::Completed`**: provider reports the order terminal-successful; `delivered_amount` is set, and `external_tx_hash` identifies the transaction on the non-Spark chain: the delivery on a send, the funding deposit on a receive. Boltz-provider conversions leave the hash unset. - **`ConversionStatus::RefundNeeded`**: the cross-chain leg was rejected after deposit (typically because the realized rate exceeded `max_slippage_bps`); the deposit is awaiting refund. *(send only)* - **`ConversionStatus::Refunded`**: the funds have been refunded back to the wallet. - **`ConversionStatus::Failed`**: terminal failure with no refund pending. A background monitor runs while the SDK is active and reconciles non-terminal payments by polling the provider. ## Send ### Quote expiry Each cross-chain send prepare response carries an `expires_at` quote-expiry timestamp on `SendPaymentMethod::CrossChainAddress`. If the quote has expired by the time you call `send_payment`, you must re-prepare to obtain a fresh quote (with a new `expires_at`) and try again. ### Retry safety Calling `send_payment` is safe to retry on transient errors **only when the send has no token-transfer leg.** Whether the source asset displayed on the route is BTC or USDB is not the determinant — what matters is the actual first leg the SDK executes. #### Sends with no token leg When the first leg is a Spark sats transfer (Orchestra with a BTC source), the SDK threads a deterministic transfer id through to the underlying Spark transfer. Retrying with the same `PrepareSendPaymentResponse` produces the same transfer id, and the Spark protocol returns the original transfer instead of firing a new one — no double-deposit. Two ways to drive idempotency: 1. **Pass a caller-supplied `idempotency_key`** on `SendPaymentRequest`. The top-level dispatcher first looks for an existing payment with that id and short-circuits the retry if found; otherwise the key is used as the Spark transfer id. 2. **Omit `idempotency_key`** — the SDK derives a deterministic UUIDv5 from the provider's quote/swap id. Re-sending the same prepared shape produces the same id and dedupes at the Spark protocol layer even if the first attempt's persistence step never completed. #### Sends with a token leg When the first leg is a token transfer at the Spark protocol layer, there is no upstream idempotency hook. The dispatcher rejects a caller-supplied `idempotency_key` with `SdkError::InvalidInput`, and a retry can fire a second token transfer and overpay. This arises in two ways for a cross-chain send: - **Direct token send** — USDB source on Orchestra. The first leg is a USDB transfer to the provider deposit address. - **Token conversion** — USDB balance routed over a route whose Spark side takes only sats. The SDK auto-converts USDB → BTC via the [stable-balance](/guide/stable_balance.md) flow before the provider leg; that conversion is itself a token transfer. This matches the existing contract for direct token sends. If you need at-most-once semantics in either of these cases, debounce retries at the application layer until the SDK either returns a payment or a terminal error. ## Receive The receiver picks a route, sets an `amount` in the source asset's base units (`route.decimals`; USD-stable parity, so `1_000_000` is one USD on any 6-decimal route), and gets back a provider-controlled deposit address to share with the sender. See [Receive payment](/guide/receive_payment.md#usdc-usdt) for the full request shape and fee-mode semantics. ### Destination selection The `destination` on `ReceivePaymentMethod::CrossChain` is a `SparkAsset` indicating which Spark-side asset the receiver wants to land. When unset, the SDK auto-picks: the wallet's active stable-balance token if the route supports landing it, otherwise BTC. An explicit choice must appear in the selected `CrossChainRoutePair.accepted_assets`. To force a specific destination, build a `SparkAsset` variant: `SparkAsset::Bitcoin` for BTC, or `SparkAsset::Token` carrying the Spark `token_identifier` (the `btkn1...` bech32m id from `CrossChainRoutePair.accepted_assets`) for USDB or any other supported token. ### Quote expiry The receive prepare response carries an `expires_at` timestamp. The SDK does not gate on it: Orchestra reprices late deposits at the live rate. The receive monitor keeps probing for the deposit for 24 hours past `expires_at` before locally closing the row as unfunded; late deposits inside that window still link correctly. ## Limitations - **Mainnet only.** Cross-chain providers operate against live external networks; there is no testnet equivalent in the SDK today. - **Background tasks required.** Both providers depend on background monitors to reconcile delivery status. `cross_chain_config` is incompatible with `background_tasks_enabled` disabled. - **Token-leg sends have no idempotency guarantee.** Applies to a direct USDB send and to any USDB-funded send that auto-converts through bitcoin. See [Retry safety](#retry-safety) above. ## Supported chains | Chain family | Asset | Chains | | ------------ | ----- | ------ | | EVM | USDC | Arbitrum One, Avalanche, Base, BSC, Codex, Ethereum, HyperEVM, Ink, Linea, Monad, Optimism, Plume, Polygon PoS, Sei, Sonic, Tempo, Unichain, World Chain, XDC | | EVM | USDT | Arbitrum One, Berachain, BSC, Conflux eSpace, Corn, Ethereum, Flare, Hedera, HyperEVM, Ink, Mantle, MegaETH, Monad, Morph, Optimism, Plasma, Polygon PoS, Rootstock, Sei, Stable, Tempo, Unichain, XLayer | | Solana | USDC | | | Solana | USDT | | | Tron | USDT | | --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Cash App to USDC/USDT](/guide/cash_app_to_usdc_usdt.md)** # Cash App to USDC/USDT payments `prepare_payment_link` lets a user send USDC or USDT to a recipient on an external chain (Ethereum-family, Solana, or Tron). The user pays through Cash App (over Lightning) and a cross-chain provider delivers the stablecoin to the recipient. The SDK acts as orchestrator only. No wallet funds move, and nothing is written to the payment history: Cash App funds the purchase externally and the provider delivers autonomously. This is distinct from the wallet-funded, tracked flow on the [USDC/USDT](/guide/cross_chain.md) page. **Developer note** Cash App is available in the US and UK only (excluding New York State for Bitcoin/Lightning features). Cash App handles region restrictions on their end, so no client-side gating is needed. ## How it works 1. Parse the recipient's external-chain address and select a destination route. 2. Call `prepare_payment_link` with the route and the USD amount. The SDK gets a quote from the cross-chain provider and returns a Cash App deep link plus the quote details. 3. Open the returned `url`. The user completes payment in Cash App, and the provider delivers the stablecoin to the recipient. Because the SDK never sees the funds, `prepare_payment_link` returns and forgets: there is no `Payment` row, no status tracking, and no event. Surface the quote fields in the response so the app can show the user what to expect before they pay. ## Selecting a route Parse the recipient address, then list the stablecoin destinations you can send to with `get_cross_chain_routes` filtered by `CrossChainRouteFilter::PaymentLink`. These routes are the ones fundable by an external rail (Cash App over Lightning), a different set from the wallet-funded send routes. Pick the `CrossChainRoutePair` whose `CrossChainRoutePair.asset` and `CrossChainRoutePair.chain` match the destination you want, and pass it to `prepare_payment_link`. Calling `prepare_payment_link` with a route that can't be funded over Lightning fails fast, before any funds move. ## Preparing the payment link Cash App funds the purchase over **Lightning**. The SDK builds a `cash.app/launch/lightning/` deep link from the provider's invoice and returns it as the response `url`. ### Rust ```rust // Parse the recipient's external-chain address (EVM/Solana/Tron). let input = ""; let InputType::CrossChainAddress(address_details) = sdk.parse(input).await? else { anyhow::bail!("Not a cross-chain address"); }; // List the stablecoin destinations you can send to and pick one, e.g. USDC // on Base. Payment-link routes are funded by Cash App over Lightning. let routes = sdk .get_cross_chain_routes(&CrossChainRouteFilter::PaymentLink { address_details: address_details.clone(), }) .await?; let route = routes .into_iter() .find(|r| r.asset == "USDC" && r.chain == "base") .ok_or_else(|| anyhow::anyhow!("No USDC route on Base"))?; // Send $10 of USDC, funded by Cash App over Lightning. The amount is in the // route asset's base units (USDC, 6 decimals), so 10_000_000 = 10 USDC, // about $10. let response = sdk .prepare_payment_link(PreparePaymentLinkRequest { address: address_details.address, route, amount: 10_000_000, fee_policy: None, max_slippage_bps: None, }) .await?; // Open this Cash App URL to pay. The recipient then receives the stablecoin. info!("Open this URL in Cash App: {}", response.url); info!( "Recipient receives ~{} {}", response.estimated_out, response.asset ); ``` ### Swift ```swift // Parse the recipient's external-chain address (EVM/Solana/Tron). let parsed = try await sdk.parse(input: "") guard case let .crossChainAddress(v1: addressDetails) = parsed else { throw NSError(domain: "PreparePaymentLink", code: 1) } // List the stablecoin destinations you can send to and pick one, e.g. USDC on Base. // Restrict to routes fundable over Lightning. let routes = try await sdk.getCrossChainRoutes( filter: .paymentLink(addressDetails: addressDetails)) guard let route = routes.first(where: { $0.asset == "USDC" && $0.chain == "base" }) else { throw NSError(domain: "PreparePaymentLink", code: 2) } // Send $10 of USDC, funded by Cash App over Lightning. The amount is in the // route asset's base units (USDC, 6 decimals), so 10_000_000 = 10 USDC, about $10. let response = try await sdk.preparePaymentLink( request: PreparePaymentLinkRequest( address: addressDetails.address, route: route, amount: 10_000_000, feePolicy: nil, maxSlippageBps: nil )) print("Open this URL in Cash App: \(response.url)") print("Recipient receives ~\(response.estimatedOut) \(response.asset)") ``` ### Kotlin ```kotlin // Parse the recipient's external-chain address (EVM/Solana/Tron). val input = "" val parsed = sdk.parse(input) if (parsed !is InputType.CrossChainAddress) { throw IllegalArgumentException("Not a cross-chain address") } val addressDetails = parsed.v1 // List the stablecoin destinations you can send to and pick one, e.g. USDC // on Base. These routes are funded by an external rail, not the Spark wallet. // Restrict to Lightning-fundable routes, since Cash App pays over Lightning. val routes = sdk.getCrossChainRoutes( CrossChainRouteFilter.PaymentLink( addressDetails = addressDetails, ) ) val route = routes.find { it.asset == "USDC" && it.chain == "base" } ?: throw IllegalArgumentException("No USDC route on Base") // Send $10 of USDC, funded by Cash App over Lightning. The amount is in // the route asset's base units (USDC, 6 decimals), so 10_000_000 = // 10 USDC, about $10. val request = PreparePaymentLinkRequest( address = addressDetails.address, route = route, amount = BigInteger.fromLong(10_000_000L), feePolicy = null, maxSlippageBps = null, ) val response = sdk.preparePaymentLink(request) // Open this Cash App URL to pay; the recipient then receives the stablecoin. // Log.v("Breez", "Open this URL in Cash App: ${response.url}") // Log.v("Breez", "Recipient receives ~${response.estimatedOut} ${response.asset}") ``` ### C# ```csharp // Parse the recipient's external-chain address (EVM/Solana/Tron). var parsed = await sdk.Parse(input: ""); if (parsed is not InputType.CrossChainAddress crossChain) { throw new InvalidOperationException("Not a cross-chain address"); } var addressDetails = crossChain.v1; // List the stablecoin destinations you can send to and pick one, // e.g. USDC on Base. Restrict to Lightning-fundable routes, // since Cash App funds over Lightning. var filter = new CrossChainRouteFilter.PaymentLink( addressDetails: addressDetails ); var routes = await sdk.GetCrossChainRoutes(filter: filter); var route = routes.First(r => r.asset == "USDC" && r.chain == "base"); // Send $10 of USDC, funded by Cash App over Lightning. The amount // is in the route asset's base units (USDC, 6 decimals), so // 10_000_000 = 10 USDC, about $10. var request = new PreparePaymentLinkRequest( address: addressDetails.address, route: route, amount: new BigInteger(10_000_000), feePolicy: null, maxSlippageBps: null ); var response = await sdk.PreparePaymentLink(request: request); Console.WriteLine($"Open this URL in Cash App: {response.url}"); Console.WriteLine($"Recipient receives ~{response.estimatedOut} {response.asset}"); ``` ### Javascript (Wasm) ```typescript // Parse the recipient's external-chain address (EVM/Solana/Tron). const parsed = await sdk.parse('') if (parsed.type !== 'crossChainAddress') { throw new Error('Not a cross-chain address') } // List the stablecoin destinations you can send to and pick one, e.g. USDC on Base. // Restrict to Lightning-fundable routes, matching Cash App's Lightning funding. const routes = await sdk.getCrossChainRoutes({ type: 'paymentLink', addressDetails: parsed }) const route = routes.find((r) => r.asset === 'USDC' && r.chain === 'base') if (route === undefined) { throw new Error('No USDC on Base route available') } // Send $10 of USDC, funded by Cash App over Lightning. The amount is in the // route asset's base units (USDC, 6 decimals), so 10_000_000 = 10 USDC, // about $10. const response = await sdk.preparePaymentLink({ address: parsed.address, route, amount: BigInt(10_000_000), feePolicy: undefined, maxSlippageBps: undefined }) console.log(`Open this URL in Cash App: ${response.url}`) console.log(`Recipient receives ~${response.estimatedOut} ${response.asset}`) ``` ### React Native ```typescript // Parse the recipient's external-chain address (EVM/Solana/Tron). const input = '' const parsed = await sdk.parse(input) if (parsed.tag !== InputType_Tags.CrossChainAddress) { throw new Error('Not a cross-chain address') } const addressDetails = parsed.inner[0] // List the stablecoin destinations you can send to and pick one, e.g. USDC // on Base. These routes are funded by an external rail, not the Spark wallet. // Restrict to Lightning-fundable routes. const routes = await sdk.getCrossChainRoutes( new CrossChainRouteFilter.PaymentLink({ addressDetails }) ) const route = routes.find((r) => r.asset === 'USDC' && r.chain === 'base') if (route === undefined) { throw new Error('No USDC route on Base') } // Send $10 of USDC, funded by Cash App over Lightning. The amount is in the // route asset's base units (USDC, 6 decimals), so 10_000_000 = 10 USDC, // about $10. const response = await sdk.preparePaymentLink({ address: addressDetails.address, route, amount: BigInt(10_000_000), feePolicy: undefined, maxSlippageBps: undefined }) // Open this Cash App URL to pay; the recipient then receives the stablecoin. console.log(`Open this URL in Cash App: ${response.url}`) console.log(`Recipient receives ~${response.estimatedOut} ${response.asset}`) ``` ### Flutter ```dart // Parse the recipient's external-chain address (EVM/Solana/Tron). String input = ""; InputType parsed = await sdk.parse(input: input); if (parsed is! InputType_CrossChainAddress) { throw Exception("Not a cross-chain address"); } CrossChainAddressDetails addressDetails = parsed.field0; // List the stablecoin destinations you can send to and pick one, e.g. USDC // on Base. These routes are funded by an external rail, not the Spark wallet. // Restrict to Lightning-fundable routes to match the Cash App flow below. List routes = await sdk.getCrossChainRoutes( filter: CrossChainRouteFilter_PaymentLink( addressDetails: addressDetails, ), ); CrossChainRoutePair route = routes.firstWhere((r) => r.asset == "USDC" && r.chain == "base"); // Send $10 of USDC, funded by Cash App over Lightning. The amount is in the // route asset's base units (USDC, 6 decimals), so 10000000 = 10 USDC, // about $10. final response = await sdk.preparePaymentLink( request: PreparePaymentLinkRequest( address: addressDetails.address, route: route, amount: BigInt.from(10000000), feePolicy: null, maxSlippageBps: null, ), ); // Open this Cash App URL to pay; the recipient then receives the stablecoin. print("Open this URL in Cash App: ${response.url}"); print("Recipient receives ~${response.estimatedOut} ${response.asset}"); ``` ### Python ```python # Parse the recipient's external-chain address (EVM/Solana/Tron). input_str = "" try: parsed = await sdk.parse(input=input_str) if not isinstance(parsed, InputType.CROSS_CHAIN_ADDRESS): raise ValueError("Not a cross-chain address") address_details = parsed[0] # List the stablecoin destinations you can send to and pick one, # e.g. USDC on Base. Restrict to Lightning-fundable routes. routes = await sdk.get_cross_chain_routes( filter=CrossChainRouteFilter.PAYMENT_LINK( address_details=address_details, ) ) route = next( (r for r in routes if r.asset == "USDC" and r.chain == "base"), None, ) if route is None: raise ValueError("No USDC route on Base") # Send $10 of USDC, funded by Cash App over Lightning. The amount is in # the route asset's base units (USDC, 6 decimals), so 10_000_000 = # 10 USDC, about $10. request = PreparePaymentLinkRequest( address=address_details.address, route=route, amount=10_000_000, fee_policy=None, max_slippage_bps=None, ) response = await sdk.prepare_payment_link(request=request) logging.debug(f"Open this URL in Cash App: {response.url}") logging.debug( f"Recipient receives ~{response.estimated_out} {response.asset}" ) except Exception as error: logging.error(error) raise ``` ### Go ```go // Parse the recipient's external-chain address (EVM/Solana/Tron). inputStr := "" input, err := sdk.Parse(inputStr) if err != nil { return nil, err } addressInput, ok := input.(breez_sdk_spark.InputTypeCrossChainAddress) if !ok { return nil, errors.New("not a cross-chain address") } addressDetails := addressInput.Field0 // List the stablecoin destinations you can send to and pick one, e.g. USDC // on Base. These routes are funded by an external rail, not the Spark wallet. // Restrict to Lightning-fundable routes, since Cash App funds over Lightning. filter := breez_sdk_spark.CrossChainRouteFilterPaymentLink{ AddressDetails: addressDetails, } routes, err := sdk.GetCrossChainRoutes(filter) if err != nil { return nil, err } var route *breez_sdk_spark.CrossChainRoutePair for i := range routes { if routes[i].Asset == "USDC" && routes[i].Chain == "base" { route = &routes[i] break } } if route == nil { return nil, errors.New("no USDC route on Base") } // Send $10 of USDC, funded by Cash App over Lightning. The amount is in the // route asset's base units (USDC, 6 decimals), so 10_000_000 = 10 USDC, // about $10. request := breez_sdk_spark.PreparePaymentLinkRequest{ Address: addressDetails.Address, Route: *route, Amount: new(big.Int).SetInt64(10_000_000), FeePolicy: nil, MaxSlippageBps: nil, } response, err := sdk.PreparePaymentLink(request) if err != nil { return nil, err } // Open this Cash App URL to pay; the recipient then receives the stablecoin. log.Printf("Open this URL in Cash App: %v", response.Url) log.Printf("Recipient receives ~%v %s", response.EstimatedOut, response.Asset) ``` ### Amounts and fees The `amount` on `PreparePaymentLinkRequest` is in the destination asset's base units, per the route's `CrossChainRoutePair.decimals`. These routes deliver USD-pegged stablecoins, so at parity it is the USD value: `1_000_000` is 1 USDC (6 decimals), about $1. `fee_policy` controls who absorbs the provider fee, reusing the same `FeePolicy` as the send flow: - `FeePolicy::FeesExcluded` (default) delivers the target amount to the recipient and adds the fee on top of what the user pays. - `FeePolicy::FeesIncluded` takes the fee out of the amount, so the user pays exactly the amount and the recipient receives less. `max_slippage_bps` bounds the price movement tolerated between quote and delivery, in basis points (1 bps = 0.01%). Left unset, the provider default applies. ### Response fields `PreparePaymentLinkResponse` carries the payment URL and the quote: | Field | Meaning | | ----- | ------- | | `url` | The Cash App deep link to open. | | `amount_sats` | Bitcoin amount the user deposits through Cash App. | | `estimated_out` | Expected amount delivered to the recipient, in `asset` units. | | `asset` | The delivered stablecoin (e.g. `USDC`). | | `service_fee_amount` | Provider fee for the conversion. | | `service_fee_asset` | Denomination of the fee. Absent means the fee is in sats. | | `expires_at` | Quote expiry. Re-call `prepare_payment_link` for a fresh quote if it lapses before the user starts paying. | ## Opening the payment link On devices with Cash App installed the URL opens the app directly; otherwise it falls back to the Cash App website. The same UX guidance as the [Buying Bitcoin](/guide/buy_bitcoin.md#recommended-ux) Cash App flow applies (mobile redirect, desktop QR code, pre-opening a tab on web to avoid popup blockers). ## Limitations - **Mainnet only.** Cash App and the cross-chain providers operate against live networks. There is no testnet equivalent. - **Not tracked.** The purchase is funded outside the wallet, so it produces no `Payment` row and no event. If you need delivery confirmation, observe the recipient chain directly. - **Orchestra routes only.** Payment links quote against Orchestra, whose orders deliver without the SDK online. A route that needs the wallet to stay online to claim before the payer's payment settles cannot back a link, so `get_cross_chain_routes` with `CrossChainRouteFilter::PaymentLink` returns only Orchestra routes. --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [End-user fees](/guide/end-user_fees.md)** # End-User Fees **The Breez SDK is free for developers.** There are small fees for end-users to send and receive payments. You can learn more about end-user fees [here](https://docs.spark.money/wallets/estimate-fees). **→ [Handling tokens](/guide/tokens.md)** # Handling tokens How to send, receive and manage issued tokens using the Breez SDK - Spark: - **[Token payments]** - Send and receive [BTKN protocol](https://docs.spark.money/learn/tokens/hello-btkn) tokens - **[Converting tokens]** - Send payments without holding the required asset by converting on-the-fly - **[Issuing tokens]** - Manage your issued [BTKN protocol](https://docs.spark.money/learn/tokens/hello-btkn) tokens [Token payments]: token_payments.md [Converting tokens]: token_conversion.md [Issuing tokens]: issuing_tokens.md **→ [Token payments](/guide/token_payments.md)** # Sending and receiving tokens Spark supports tokens using the [BTKN protocol](https://docs.spark.money/learn/tokens/hello-btkn). The Breez SDK enables you to send and receive these tokens using the standard payments API. ## Fetching token balances API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.get_info Token balances for all tokens currently held in the wallet can be retrieved along with general wallet information. Each token balance includes both the balance amount and the token metadata (identifier, name, ticker, issuer public key, etc.). ### Rust ```rust let info = sdk .get_info(GetInfoRequest { // ensure_synced: true will ensure the SDK is synced with the Spark network // before returning the balance ensure_synced: Some(false), }) .await?; // Token balances are a map of token identifier to balance let token_balances = info.token_balances; for (token_id, token_balance) in token_balances { info!("Token ID: {}", token_id); info!("Balance: {}", token_balance.balance); info!("Name: {}", token_balance.token_metadata.name); info!("Ticker: {}", token_balance.token_metadata.ticker); info!("Decimals: {}", token_balance.token_metadata.decimals); } ``` ### Swift ```swift // ensureSynced: true will ensure the SDK is synced with the Spark network // before returning the balance let info = try await sdk.getInfo( request: GetInfoRequest( ensureSynced: false )) // Token balances are a map of token identifier to balance let tokenBalances = info.tokenBalances for (tokenId, tokenBalance) in tokenBalances { print("Token ID: \(tokenId)") print("Balance: \(tokenBalance.balance)") print("Name: \(tokenBalance.tokenMetadata.name)") print("Ticker: \(tokenBalance.tokenMetadata.ticker)") print("Decimals: \(tokenBalance.tokenMetadata.decimals)") } ``` ### Kotlin ```kotlin try { // ensureSynced: true will ensure the SDK is synced with the Spark network // before returning the balance val info = sdk.getInfo(GetInfoRequest(false)) // Token balances are a map of token identifier to balance val tokenBalances = info.tokenBalances for ((tokenId, tokenBalance) in tokenBalances) { println("Token ID: $tokenId") println("Balance: ${tokenBalance.balance}") println("Name: ${tokenBalance.tokenMetadata.name}") println("Ticker: ${tokenBalance.tokenMetadata.ticker}") println("Decimals: ${tokenBalance.tokenMetadata.decimals}") } } catch (e: Exception) { // handle error } ``` ### C# ```csharp // ensureSynced: true will ensure the SDK is synced with the Spark network // before returning the balance var info = await sdk.GetInfo(request: new GetInfoRequest(ensureSynced: false)); // Token balances are a map of token identifier to balance var tokenBalances = info.tokenBalances; foreach (var kvp in tokenBalances) { var tokenId = kvp.Key; var tokenBalance = kvp.Value; Console.WriteLine($"Token ID: {tokenId}"); Console.WriteLine($"Balance: {tokenBalance.balance}"); Console.WriteLine($"Name: {tokenBalance.tokenMetadata.name}"); Console.WriteLine($"Ticker: {tokenBalance.tokenMetadata.ticker}"); Console.WriteLine($"Decimals: {tokenBalance.tokenMetadata.decimals}"); } ``` ### Javascript (Wasm) ```typescript const info = await sdk.getInfo({ // ensureSynced: true will ensure the SDK is synced with the Spark network // before returning the balance ensureSynced: false }) // Token balances are a map of token identifier to balance const tokenBalances = info.tokenBalances for (const [tokenId, tokenBalance] of Object.entries(tokenBalances)) { console.log(`Token ID: ${tokenId}`) console.log(`Balance: ${tokenBalance.balance}`) console.log(`Name: ${tokenBalance.tokenMetadata.name}`) console.log(`Ticker: ${tokenBalance.tokenMetadata.ticker}`) console.log(`Decimals: ${tokenBalance.tokenMetadata.decimals}`) } ``` ### React Native ```typescript const info = await sdk.getInfo({ // ensureSynced: true will ensure the SDK is synced with the Spark network // before returning the balance ensureSynced: false }) // Token balances are a map of token identifier to balance const tokenBalances = info.tokenBalances for (const [tokenId, tokenBalance] of Object.entries(tokenBalances)) { console.log(`Token ID: ${tokenId}`) console.log(`Balance: ${tokenBalance.balance}`) console.log(`Name: ${tokenBalance.tokenMetadata.name}`) console.log(`Ticker: ${tokenBalance.tokenMetadata.ticker}`) console.log(`Decimals: ${tokenBalance.tokenMetadata.decimals}`) } ``` ### Flutter ```dart // ensureSynced: true will ensure the SDK is synced with the Spark network // before returning the balance final info = await sdk.getInfo(request: GetInfoRequest(ensureSynced: false)); // Token balances are a map of token identifier to balance final tokenBalances = info.tokenBalances; tokenBalances.forEach((tokenId, tokenBalance) { print('Token ID: $tokenId'); print('Balance: ${tokenBalance.balance}'); print('Name: ${tokenBalance.tokenMetadata.name}'); print('Ticker: ${tokenBalance.tokenMetadata.ticker}'); print('Decimals: ${tokenBalance.tokenMetadata.decimals}'); }); ``` ### Python ```python try: # ensure_synced: True will ensure the SDK is synced with the Spark network # before returning the balance info = await sdk.get_info(request=GetInfoRequest(ensure_synced=False)) # Token balances are a map of token identifier to balance token_balances = info.token_balances for token_id, token_balance in token_balances.items(): print(f"Token ID: {token_id}") print(f"Balance: {token_balance.balance}") print(f"Name: {token_balance.token_metadata.name}") print(f"Ticker: {token_balance.token_metadata.ticker}") print(f"Decimals: {token_balance.token_metadata.decimals}") except Exception as error: logging.error(error) raise ``` ### Go ```go ensureSynced := false info, err := sdk.GetInfo(breez_sdk_spark.GetInfoRequest{ // EnsureSynced: true will ensure the SDK is synced with the Spark network // before returning the balance EnsureSynced: &ensureSynced, }) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } // Token balances are a map of token identifier to balance tokenBalances := info.TokenBalances for tokenId, tokenBalance := range tokenBalances { log.Printf("Token ID: %v", tokenId) log.Printf("Balance: %v", tokenBalance.Balance) log.Printf("Name: %v", tokenBalance.TokenMetadata.Name) log.Printf("Ticker: %v", tokenBalance.TokenMetadata.Ticker) log.Printf("Decimals: %v", tokenBalance.TokenMetadata.Decimals) } ``` **Developer note** Token balances are cached for fast responses. For details on ensuring up-to-date balances, see the Fetching the balance section. ## Fetching token metadata API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.get_tokens_metadata Token metadata can be fetched for specific tokens by providing their identifiers. This is especially useful for retrieving metadata for tokens that are not currently held in the wallet. The metadata is cached locally after the first fetch for faster subsequent lookups. ### Rust ```rust let response = sdk .get_tokens_metadata(GetTokensMetadataRequest { token_identifiers: vec![ String::from(""), String::from(""), ], }) .await?; let tokens_metadata = response.tokens_metadata; for token_metadata in tokens_metadata { info!("Token ID: {}", token_metadata.identifier); info!("Name: {}", token_metadata.name); info!("Ticker: {}", token_metadata.ticker); info!("Decimals: {}", token_metadata.decimals); info!("Max Supply: {}", token_metadata.max_supply); info!("Is Freezable: {}", token_metadata.is_freezable); } ``` ### Swift ```swift let response = try await sdk.getTokensMetadata( request: GetTokensMetadataRequest(tokenIdentifiers: [ "", "", ])) let tokensMetadata = response.tokensMetadata for tokenMetadata in tokensMetadata { print("Token ID: \(tokenMetadata.identifier)") print("Name: \(tokenMetadata.name)") print("Ticker: \(tokenMetadata.ticker)") print("Decimals: \(tokenMetadata.decimals)") print("Max Supply: \(tokenMetadata.maxSupply)") print("Is Freezable: \(tokenMetadata.isFreezable)") } ``` ### Kotlin ```kotlin try { val response = sdk.getTokensMetadata( GetTokensMetadataRequest( tokenIdentifiers = listOf("", "") ) ) val tokensMetadata = response.tokensMetadata for (tokenMetadata in tokensMetadata) { println("Token ID: ${tokenMetadata.identifier}") println("Name: ${tokenMetadata.name}") println("Ticker: ${tokenMetadata.ticker}") println("Decimals: ${tokenMetadata.decimals}") println("Max Supply: ${tokenMetadata.maxSupply}") println("Is Freezable: ${tokenMetadata.isFreezable}") } } catch (e: Exception) { // handle error } ``` ### C# ```csharp var response = await sdk.GetTokensMetadata( request: new GetTokensMetadataRequest( tokenIdentifiers: new string[] { "", "" } ) ); var tokensMetadata = response.tokensMetadata; foreach (var tokenMetadata in tokensMetadata) { Console.WriteLine($"Token ID: {tokenMetadata.identifier}"); Console.WriteLine($"Name: {tokenMetadata.name}"); Console.WriteLine($"Ticker: {tokenMetadata.ticker}"); Console.WriteLine($"Decimals: {tokenMetadata.decimals}"); Console.WriteLine($"Max Supply: {tokenMetadata.maxSupply}"); Console.WriteLine($"Is Freezable: {tokenMetadata.isFreezable}"); } ``` ### Javascript (Wasm) ```typescript const response = await sdk.getTokensMetadata({ tokenIdentifiers: ['', ''] }) const tokensMetadata = response.tokensMetadata for (const tokenMetadata of tokensMetadata) { console.log(`Token ID: ${tokenMetadata.identifier}`) console.log(`Name: ${tokenMetadata.name}`) console.log(`Ticker: ${tokenMetadata.ticker}`) console.log(`Decimals: ${tokenMetadata.decimals}`) console.log(`Max Supply: ${tokenMetadata.maxSupply}`) console.log(`Is Freezable: ${tokenMetadata.isFreezable}`) } ``` ### React Native ```typescript const response = await sdk.getTokensMetadata({ tokenIdentifiers: ['', ''] }) const tokensMetadata = response.tokensMetadata for (const tokenMetadata of tokensMetadata) { console.log(`Token ID: ${tokenMetadata.identifier}`) console.log(`Name: ${tokenMetadata.name}`) console.log(`Ticker: ${tokenMetadata.ticker}`) console.log(`Decimals: ${tokenMetadata.decimals}`) console.log(`Max Supply: ${tokenMetadata.maxSupply}`) console.log(`Is Freezable: ${tokenMetadata.isFreezable}`) } ``` ### Flutter ```dart final response = await sdk.getTokensMetadata( request: GetTokensMetadataRequest( tokenIdentifiers: ['', ''] ) ); final tokensMetadata = response.tokensMetadata; for (final tokenMetadata in tokensMetadata) { print('Token ID: $tokenMetadata.identifier'); print('Name: ${tokenMetadata.name}'); print('Ticker: ${tokenMetadata.ticker}'); print('Decimals: ${tokenMetadata.decimals}'); print('Max Supply: ${tokenMetadata.maxSupply}'); print('Is Freezable: ${tokenMetadata.isFreezable}'); } ``` ### Python ```python try: response = await sdk.get_tokens_metadata( request=GetTokensMetadataRequest( token_identifiers=["", ""] ) ) tokens_metadata = response.tokens_metadata for token_metadata in tokens_metadata: print(f"Token ID: {token_metadata.identifier}") print(f"Name: {token_metadata.name}") print(f"Ticker: {token_metadata.ticker}") print(f"Decimals: {token_metadata.decimals}") print(f"Max Supply: {token_metadata.max_supply}") print(f"Is Freezable: {token_metadata.is_freezable}") except Exception as error: logging.error(error) raise ``` ### Go ```go tokenIdentifiers := []string{"", ""} response, err := sdk.GetTokensMetadata(breez_sdk_spark.GetTokensMetadataRequest{ TokenIdentifiers: tokenIdentifiers, }) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } tokensMetadata := response.TokensMetadata for _, tokenMetadata := range tokensMetadata { log.Printf("Token ID: %v", tokenMetadata.Identifier) log.Printf("Name: %v", tokenMetadata.Name) log.Printf("Ticker: %v", tokenMetadata.Ticker) log.Printf("Decimals: %v", tokenMetadata.Decimals) log.Printf("Max Supply: %v", tokenMetadata.MaxSupply) log.Printf("Is Freezable: %v", tokenMetadata.IsFreezable) } ``` ## Receiving a token payment API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.receive_payment Token payments can be received using either a Spark address or invoice. Using an invoice is useful to impose restrictions on the payment, such as the token to receive, amount, expiry, and who can pay it. ### Spark address Token payments use the same Spark address as Bitcoin payments - no separate address is required. Your application can retrieve the Spark address as described in the [Receiving a payment](/guide/receive_payment.md#spark) guide. The payer will use this address to send tokens to the wallet. ### Spark invoice Spark token invoices can be created using the same API as Bitcoin Spark invoices. The only difference is that a token identifier is provided. #### Rust ```rust let token_identifier = Some("".to_string()); let optional_description = Some("".to_string()); let optional_amount = Some(5_000); // Optionally set the expiry UNIX timestamp in seconds let optional_expiry_time_seconds = Some(1716691200); let optional_sender_public_key = Some("".to_string()); let response = sdk .receive_payment(ReceivePaymentRequest { payment_method: ReceivePaymentMethod::SparkInvoice { token_identifier, description: optional_description, amount: optional_amount, expiry_time: optional_expiry_time_seconds, sender_public_key: optional_sender_public_key, }, }) .await?; let payment_request = response.payment_request; info!("Payment request: {payment_request}"); let receive_fee = response.fee; info!("Fees: {receive_fee} token base units"); ``` #### Swift ```swift let tokenIdentifier = "" let optionalDescription = "" let optionalAmount = BInt(5_000) // Optionally set the expiry UNIX timestamp in seconds let optionalExpiryTimeSeconds: UInt64 = 1_716_691_200 let optionalSenderPublicKey = "" let response = try await sdk .receivePayment( request: ReceivePaymentRequest( paymentMethod: ReceivePaymentMethod.sparkInvoice( amount: optionalAmount, tokenIdentifier: tokenIdentifier, expiryTime: optionalExpiryTimeSeconds, description: optionalDescription, senderPublicKey: optionalSenderPublicKey ) )) let paymentRequest = response.paymentRequest print("Payment request: \(paymentRequest)") let receiveFeeSats = response.fee print("Fees: \(receiveFeeSats) token base units") ``` #### Kotlin ```kotlin try { val tokenIdentifier = "" val optionalDescription = "" // Kotlin MPP (BigInteger from com.ionspin.kotlin.bignum.integer, which is included in // package) val optionalAmount = BigInteger.fromLong(5_000L) // Android (BigInteger from java.math) // val optionalAmount = BigInteger.valueOf(5_000L) // Optionally set the expiry UNIX timestamp in seconds val optionalExpiryTimeSeconds = 1716691200.toULong() val optionalSenderPublicKey = "" val request = ReceivePaymentRequest( ReceivePaymentMethod.SparkInvoice( tokenIdentifier = tokenIdentifier, description = optionalDescription, amount = optionalAmount, expiryTime = optionalExpiryTimeSeconds, senderPublicKey = optionalSenderPublicKey ) ) val response = sdk.receivePayment(request) val paymentRequest = response.paymentRequest println("Payment request: $paymentRequest") val receiveFee = response.fee println("Fees: $receiveFee token base units") } catch (e: Exception) { // handle error } ``` #### C# ```csharp var tokenIdentifier = ""; var optionalDescription = ""; var optionalAmount = new BigInteger(5000); // Optionally set the expiry UNIX timestamp in seconds var optionalExpiryTimeSeconds = 1716691200UL; var optionalSenderPublicKey = ""; var request = new ReceivePaymentRequest( paymentMethod: new ReceivePaymentMethod.SparkInvoice( tokenIdentifier: tokenIdentifier, description: optionalDescription, amount: optionalAmount, expiryTime: optionalExpiryTimeSeconds, senderPublicKey: optionalSenderPublicKey ) ); var response = await sdk.ReceivePayment(request: request); var paymentRequest = response.paymentRequest; Console.WriteLine($"Payment request: {paymentRequest}"); var receiveFee = response.fee; Console.WriteLine($"Fees: {receiveFee} token base units"); ``` #### Javascript (Wasm) ```typescript const tokenIdentifier = '' const optionalDescription = '' const optionalAmount = '5000' // Optionally set the expiry UNIX timestamp in seconds const optionalExpiryTimeSeconds = 1716691200 const optionalSenderPublicKey = '' const response = await sdk.receivePayment({ paymentMethod: { type: 'sparkInvoice', tokenIdentifier, description: optionalDescription, amount: optionalAmount, expiryTime: optionalExpiryTimeSeconds, senderPublicKey: optionalSenderPublicKey } }) const paymentRequest = response.paymentRequest console.log(`Payment request: ${paymentRequest}`) const receiveFeeSats = response.fee console.log(`Fees: ${receiveFeeSats} token base units`) ``` #### React Native ```typescript const tokenIdentifier = '' const optionalDescription = '' const optionalAmount = BigInt(5_000) // Optionally set the expiry UNIX timestamp in seconds const optionalExpiryTimeSeconds = BigInt(1716691200) const optionalSenderPublicKey = '' const response = await sdk.receivePayment({ paymentMethod: new ReceivePaymentMethod.SparkInvoice({ tokenIdentifier, description: optionalDescription, amount: optionalAmount, expiryTime: optionalExpiryTimeSeconds, senderPublicKey: optionalSenderPublicKey }) }) const paymentRequest = response.paymentRequest console.log(`Payment request: ${paymentRequest}`) const receiveFee = response.fee console.log(`Fees: ${receiveFee} token base units`) ``` #### Flutter ```dart String tokenIdentifier = ''; String optionalDescription = ""; BigInt optionalAmount = BigInt.from(5000); // Optionally set the expiry UNIX timestamp in seconds BigInt optionalExpiryTimeSeconds = BigInt.from(1716691200); String optionalSenderPublicKey = ""; ReceivePaymentRequest request = ReceivePaymentRequest(paymentMethod: ReceivePaymentMethod.sparkInvoice( tokenIdentifier: tokenIdentifier, description: optionalDescription, amount: optionalAmount, expiryTime: optionalExpiryTimeSeconds, senderPublicKey: optionalSenderPublicKey, )); ReceivePaymentResponse response = await sdk.receivePayment( request: request, ); String paymentRequest = response.paymentRequest; print("Payment request: $paymentRequest"); BigInt receiveFee = response.fee; print("Fees: $receiveFee token base units"); ``` #### Python ```python try: token_identifier = "" optional_description = "" optional_amount = 5_000 # Optionally set the expiry UNIX timestamp in seconds optional_expiry_time_seconds = 1716691200 optional_sender_public_key = "" request = ReceivePaymentRequest( payment_method=ReceivePaymentMethod.SPARK_INVOICE( token_identifier=token_identifier, description=optional_description, amount=optional_amount, expiry_time=optional_expiry_time_seconds, sender_public_key=optional_sender_public_key, ) ) response = await sdk.receive_payment(request=request) payment_request = response.payment_request print(f"Payment request: {payment_request}") receive_fee = response.fee print(f"Fees: {receive_fee} token base units") return response except Exception as error: logging.error(error) raise ``` #### Go ```go tokenIdentifier := "" optionalDescription := "" optionalAmount := new(big.Int).SetInt64(5_000) // Optionally set the expiry UNIX timestamp in seconds optionalExpiryTimeSeconds := uint64(1716691200) optionalSenderPublicKey := "" request := breez_sdk_spark.ReceivePaymentRequest{ PaymentMethod: breez_sdk_spark.ReceivePaymentMethodSparkInvoice{ TokenIdentifier: &tokenIdentifier, Description: &optionalDescription, Amount: &optionalAmount, ExpiryTime: &optionalExpiryTimeSeconds, SenderPublicKey: &optionalSenderPublicKey, }, } response, err := sdk.ReceivePayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } paymentRequest := response.PaymentRequest log.Printf("Payment Request: %v", paymentRequest) receiveFees := response.Fee log.Printf("Fees: %v token base units", receiveFees) ``` ## Sending a token payment API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.prepare_send_payment To send tokens, provide a Spark address as the payment request. The token identifier must be specified in one of two ways: 1. **Using a Spark invoice**: If the payee provides a Spark address with an embedded token identifier and amount (a Spark invoice), the SDK automatically extracts and uses those values. 2. **Manual specification**: For a plain Spark address without embedded payment details, your application must provide both the token identifier and amount parameters when preparing the payment. Your application can use the [parse](/guide/parse.md) functionality to determine if a Spark address contains embedded token payment details before preparing the payment. The code example below demonstrates manual specification. Follow the standard prepare/send payment flow as described in the [Sending a payment](/guide/send_payment.md) guide. **Developer note** Payments can be sent without holding an asset by converting on-the-fly as a step before sending a payment. See Converting tokens for more information. ### Rust ```rust let payment_request = "".to_string(); // Token identifier must match the invoice in case it specifies one. let token_identifier = Some("".to_string()); // Set the amount of tokens you wish to send (in token base units). let amount = Some(1_000); let prepare_response = sdk .prepare_send_payment(PrepareSendPaymentRequest { payment_request: PaymentRequest::Input { input: payment_request, }, amount, token_identifier, conversion_options: None, fee_policy: None, }) .await?; // If the fees are acceptable, continue to send the token payment match &prepare_response.payment_method { SendPaymentMethod::SparkAddress { fee, token_identifier: token_id, .. } => { info!("Token ID: {:?}", token_id); info!("Fees: {} token base units", fee); } SendPaymentMethod::SparkInvoice { fee, token_identifier: token_id, .. } => { info!("Token ID: {:?}", token_id); info!("Fees: {} token base units", fee); } _ => {} } // Send the token payment let send_response = sdk .send_payment(SendPaymentRequest { prepare_response, options: None, idempotency_key: None, }) .await?; let payment = send_response.payment; info!("Payment: {payment:?}"); ``` ### Swift ```swift let paymentRequest = "" // Token identifier must match the invoice in case it specifies one. let tokenIdentifier: String? = "" // Set the amount of tokens you wish to send. let amount: BInt? = BInt(1_000) let prepareResponse = try await sdk.prepareSendPayment( request: PrepareSendPaymentRequest( paymentRequest: .input(input: paymentRequest), amount: amount, tokenIdentifier: tokenIdentifier, conversionOptions: nil, feePolicy: nil )) // If the fees are acceptable, continue to send the token payment if case let .sparkAddress(address, fee, tokenId) = prepareResponse.paymentMethod { print("Token ID: \(String(describing: tokenId))") print("Fees: \(fee) token base units") } if case let .sparkInvoice(invoice, fee, tokenId) = prepareResponse.paymentMethod { print("Token ID: \(String(describing: tokenId))") print("Fees: \(fee) token base units") } // Send the token payment let sendResponse = try await sdk.sendPayment( request: SendPaymentRequest( prepareResponse: prepareResponse, options: nil )) let payment = sendResponse.payment print("Payment: \(payment)") ``` ### Kotlin ```kotlin try { val paymentRequest = "" // Token identifier must match the invoice in case it specifies one. val tokenIdentifier = "" // Set the amount of tokens you wish to send (in token base units). // Kotlin MPP (BigInteger from com.ionspin.kotlin.bignum.integer) val amount = BigInteger.fromLong(1_000L) // Android (BigInteger from java.math) // val amount = BigInteger.valueOf(1_000L) val prepareResponse = sdk.prepareSendPayment( PrepareSendPaymentRequest( paymentRequest = PaymentRequest.Input(input = paymentRequest), amount = amount, tokenIdentifier = tokenIdentifier, conversionOptions = null, feePolicy = null, ) ) // If the fees are acceptable, continue to send the token payment when (val method = prepareResponse.paymentMethod) { is SendPaymentMethod.SparkAddress -> { println("Token ID: ${method.tokenIdentifier}") println("Fees: ${method.fee} token base units") } is SendPaymentMethod.SparkInvoice -> { println("Token ID: ${method.tokenIdentifier}") println("Fees: ${method.fee} token base units") } else -> {} } // Send the token payment val sendResponse = sdk.sendPayment( SendPaymentRequest(prepareResponse = prepareResponse, options = null) ) val payment = sendResponse.payment println("Payment: $payment") } catch (e: Exception) { // handle error } ``` ### C# ```csharp var paymentRequest = ""; // Token identifier must match the invoice in case it specifies one. var tokenIdentifier = ""; // Set the amount of tokens you wish to send. ulong? amount = 1_000UL; var prepareResponse = await sdk.PrepareSendPayment( request: new PrepareSendPaymentRequest( paymentRequest: new PaymentRequest.Input(input: paymentRequest), amount: amount, tokenIdentifier: tokenIdentifier, conversionOptions: null, feePolicy: null ) ); // If the fees are acceptable, continue to send the token payment if (prepareResponse.paymentMethod is SendPaymentMethod.SparkAddress sparkAddress) { Console.WriteLine($"Token ID: {sparkAddress.tokenIdentifier}"); Console.WriteLine($"Fees: {sparkAddress.fee} token base units"); } if (prepareResponse.paymentMethod is SendPaymentMethod.SparkInvoice sparkInvoice) { Console.WriteLine($"Token ID: {sparkInvoice.tokenIdentifier}"); Console.WriteLine($"Fees: {sparkInvoice.fee} token base units"); } // Send the token payment var sendResponse = await sdk.SendPayment( request: new SendPaymentRequest( prepareResponse: prepareResponse, options: null ) ); var payment = sendResponse.payment; Console.WriteLine($"Payment: {payment}"); ``` ### Javascript (Wasm) ```typescript const paymentRequest = '' // Token identifier must match the invoice in case it specifies one. const tokenIdentifier = '' // Set the amount of tokens you wish to send (in token base units). const amount = BigInt(1_000) const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: { type: 'input', input: paymentRequest }, amount, tokenIdentifier, conversionOptions: undefined, feePolicy: undefined }) // If the fees are acceptable, continue to send the token payment if (prepareResponse.paymentMethod.type === 'sparkAddress') { console.log(`Token ID: ${prepareResponse.paymentMethod.tokenIdentifier}`) console.log(`Fees: ${prepareResponse.paymentMethod.fee} token base units`) } if (prepareResponse.paymentMethod.type === 'sparkInvoice') { console.log(`Token ID: ${prepareResponse.paymentMethod.tokenIdentifier}`) console.log(`Fees: ${prepareResponse.paymentMethod.fee} token base units`) } // Send the token payment const sendResponse = await sdk.sendPayment({ prepareResponse, options: undefined }) const payment = sendResponse.payment console.log(`Payment: ${JSON.stringify(payment)}`) ``` ### React Native ```typescript const paymentRequest = '' // Token identifier must match the invoice in case it specifies one. const tokenIdentifier = '' // Set the amount of tokens you wish to send (in token base units). const amount = BigInt(1_000) const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: new PaymentRequest.Input({ input: paymentRequest }), amount, tokenIdentifier, conversionOptions: undefined, feePolicy: undefined }) // If the fees are acceptable, continue to send the token payment if (prepareResponse.paymentMethod?.tag === SendPaymentMethod_Tags.SparkAddress) { console.log(`Token ID: ${prepareResponse.paymentMethod.inner.tokenIdentifier}`) console.log(`Fees: ${prepareResponse.paymentMethod.inner.fee} token base units`) } if (prepareResponse.paymentMethod?.tag === SendPaymentMethod_Tags.SparkInvoice) { console.log(`Token ID: ${prepareResponse.paymentMethod.inner.tokenIdentifier}`) console.log(`Fees: ${prepareResponse.paymentMethod.inner.fee} token base units`) } // Send the token payment const sendResponse = await sdk.sendPayment({ prepareResponse, options: undefined, idempotencyKey: undefined }) const payment = sendResponse.payment console.log(`Payment: ${JSON.stringify(payment)}`) ``` ### Flutter ```dart final paymentRequest = ''; // Token identifier must match the invoice in case it specifies one. final tokenIdentifier = ''; // Set the amount of tokens you wish to send (in token base units). final amount = BigInt.from(1000); final prepareResponse = await sdk.prepareSendPayment( request: PrepareSendPaymentRequest( paymentRequest: PaymentRequest.input(input: paymentRequest), amount: amount, tokenIdentifier: tokenIdentifier, conversionOptions: null, feePolicy: null, ), ); // If the fees are acceptable, continue to send the token payment if (prepareResponse.paymentMethod is SendPaymentMethod_SparkAddress) { final method = prepareResponse.paymentMethod as SendPaymentMethod_SparkAddress; print('Token ID: ${method.tokenIdentifier}'); print('Fees: ${method.fee} token base units'); } if (prepareResponse.paymentMethod is SendPaymentMethod_SparkInvoice) { final method = prepareResponse.paymentMethod as SendPaymentMethod_SparkInvoice; print('Token ID: ${method.tokenIdentifier}'); print('Fees: ${method.fee} token base units'); } // Send the token payment final sendResponse = await sdk.sendPayment( request: SendPaymentRequest( prepareResponse: prepareResponse, options: null, ), ); final payment = sendResponse.payment; print('Payment: $payment'); ``` ### Python ```python try: payment_request = "" token_identifier = "" amount = 1_000 prepare_response = await sdk.prepare_send_payment( request=PrepareSendPaymentRequest( payment_request=PaymentRequest.INPUT(input=payment_request), amount=amount, token_identifier=token_identifier, conversion_options=None, fee_policy=None, ) ) # If the fees are acceptable, continue to send the token payment if isinstance(prepare_response.payment_method, SendPaymentMethod.SPARK_ADDRESS): print(f"Token ID: {prepare_response.payment_method.token_identifier}") print(f"Fees: {prepare_response.payment_method.fee} token base units") if isinstance(prepare_response.payment_method, SendPaymentMethod.SPARK_INVOICE): print(f"Token ID: {prepare_response.payment_method.token_identifier}") print(f"Fees: {prepare_response.payment_method.fee} token base units") # Send the token payment send_response = await sdk.send_payment( request=SendPaymentRequest( prepare_response=prepare_response, options=None, ) ) payment = send_response.payment print(f"Payment: {payment}") except Exception as error: logging.error(error) raise ``` ### Go ```go paymentRequest := "" // Token identifier must match the invoice in case it specifies one. tokenIdentifier := "" // Set the amount of tokens you wish to send. amount := new(big.Int).SetInt64(1_000) prepareResponse, err := sdk.PrepareSendPayment(breez_sdk_spark.PrepareSendPaymentRequest{ PaymentRequest: breez_sdk_spark.PaymentRequestInput{Input: paymentRequest}, Amount: &amount, TokenIdentifier: &tokenIdentifier, ConversionOptions: nil, FeePolicy: nil, }) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } // If the fees are acceptable, continue to send the token payment switch method := prepareResponse.PaymentMethod.(type) { case breez_sdk_spark.SendPaymentMethodSparkAddress: log.Printf("Token ID: %v", method.TokenIdentifier) log.Printf("Fees: %v token base units", method.Fee) case breez_sdk_spark.SendPaymentMethodSparkInvoice: log.Printf("Token ID: %v", method.TokenIdentifier) log.Printf("Fees: %v token base units", method.Fee) } // Send the token payment sendResponse, err := sdk.SendPayment(breez_sdk_spark.SendPaymentRequest{ PrepareResponse: prepareResponse, Options: nil, }) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } payment := sendResponse.Payment log.Printf("Payment: %#v", payment) ``` To pay several recipients at once, see [Sending to multiple recipients](/guide/batch_send.md): one transaction can pay multiple payees, across several tokens, mixing Spark addresses and invoices. A batch that pays an invoice stays on one token. ## Listing token payments API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.list_payments Token payments are included in the regular payment history alongside Bitcoin payments. Your application can retrieve and distinguish token payments from other payment types using the standard payment listing functionality. See the [Listing payments](/guide/list_payments.md) guide for more details. **→ [Sending to multiple recipients](/guide/batch_send.md)** # Sending to multiple recipients A single transaction can pay multiple recipients at once. The batch API is separate from the regular send flow because a batch has no single amount to report: it may span several tokens, so prepare reports a total per asset instead. A batch currently pays tokens only. Sending sats to several recipients at once is not supported yet, so every recipient must resolve to a token. ## Preparing the batch API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.prepare_send_batch Each recipient is identified by a `payment_request`, which is either a Spark address or a Spark invoice, and the two may be mixed freely in one batch: - **Spark address**: the token identifier and the amount must be set, exactly as for a single-recipient send. Leaving the token identifier unset would name sats, which a batch cannot send yet. - **Spark invoice**: the token and amount are taken from the invoice. The amount is only required if the invoice doesn't specify one. If the invoice specifies an amount, providing a different amount is not supported. The same invoice may only appear once in a batch. Repeating a plain Spark address is allowed: that is simply two outputs to the same payee. A batch that pays a Spark invoice is limited to a single token, and that includes its address recipients: the Spark operators reject a transaction that carries an invoice and pays more than one token. Prepare rejects such a batch, so split it into one batch per token. A batch of address recipients only may span as many tokens as you like. The response resolves every recipient into the concrete `destination`, asset and amount it will be paid, and reports `totals`: what the batch debits, one entry per distinct asset. You may show these to the user before sending. ### Rust ```rust // Each recipient is a Spark address or a Spark invoice. An invoice that // names its own token and amount needs neither here. let recipients = vec![ BatchRecipient { payment_request: "".to_string(), amount: Some(1_000), token_identifier: Some("".to_string()), }, BatchRecipient { payment_request: "".to_string(), amount: None, token_identifier: None, }, ]; let prepare_response = sdk .prepare_send_batch(PrepareSendBatchRequest { recipients }) .await?; // Show what the batch debits, one entry per token for total in &prepare_response.totals { // Unset would mean sats, which a batch cannot send yet let token_id = total.token_identifier.as_deref().unwrap_or_default(); info!("Token ID: {token_id}"); info!("Total: {} token base units", total.amount); } // If the totals are acceptable, send the batch let send_response = sdk .send_batch(SendBatchRequest { prepare_response }) .await?; // One payment per recipient, in the order they were requested for payment in send_response.payments { info!("Payment: {payment:?}"); } ``` ### Swift ```swift // Each recipient is a Spark address or a Spark invoice. An invoice that // names its own token and amount needs neither here. let recipients = [ BatchRecipient( paymentRequest: "", amount: BInt(1_000), tokenIdentifier: "" ), BatchRecipient( paymentRequest: "", amount: nil, tokenIdentifier: nil ), ] let prepareResponse = try await sdk.prepareSendBatch( request: PrepareSendBatchRequest(recipients: recipients)) // Show what the batch debits, one entry per token for total in prepareResponse.totals { // Unset would mean sats, which a batch cannot send yet print("Token ID: \(total.tokenIdentifier ?? "")") print("Total: \(total.amount) token base units") } // If the totals are acceptable, send the batch let sendResponse = try await sdk.sendBatch( request: SendBatchRequest(prepareResponse: prepareResponse)) // One payment per recipient, in the order they were requested for payment in sendResponse.payments { print("Payment: \(payment)") } ``` ### Kotlin ```kotlin try { // Each recipient is a Spark address or a Spark invoice. An invoice that // names its own token and amount needs neither here. val recipients = listOf( BatchRecipient( paymentRequest = "", // Kotlin MPP (BigInteger from com.ionspin.kotlin.bignum.integer) amount = BigInteger.fromLong(1_000L), // Android (BigInteger from java.math) // amount = BigInteger.valueOf(1_000L), tokenIdentifier = "" ), BatchRecipient( paymentRequest = "", amount = null, tokenIdentifier = null ) ) val prepareResponse = sdk.prepareSendBatch(PrepareSendBatchRequest(recipients = recipients)) // Show what the batch debits, one entry per token for (total in prepareResponse.totals) { // Unset would mean sats, which a batch cannot send yet println("Token ID: ${total.tokenIdentifier}") println("Total: ${total.amount} token base units") } // If the totals are acceptable, send the batch val sendResponse = sdk.sendBatch(SendBatchRequest(prepareResponse = prepareResponse)) // One payment per recipient, in the order they were requested for (payment in sendResponse.payments) { println("Payment: $payment") } } catch (e: Exception) { // handle error } ``` ### C# ```csharp // Each recipient is a Spark address or a Spark invoice. An invoice that // names its own token and amount needs neither here. var recipients = new BatchRecipient[] { new BatchRecipient( paymentRequest: "", amount: new BigInteger(1_000), tokenIdentifier: "" ), new BatchRecipient( paymentRequest: "", amount: null, tokenIdentifier: null ) }; var prepareResponse = await sdk.PrepareSendBatch( request: new PrepareSendBatchRequest(recipients: recipients) ); // Show what the batch debits, one entry per token foreach (var total in prepareResponse.totals) { // Unset would mean sats, which a batch cannot send yet Console.WriteLine($"Token ID: {total.tokenIdentifier}"); Console.WriteLine($"Total: {total.amount} token base units"); } // If the totals are acceptable, send the batch var sendResponse = await sdk.SendBatch( request: new SendBatchRequest(prepareResponse: prepareResponse) ); // One payment per recipient, in the order they were requested foreach (var payment in sendResponse.payments) { Console.WriteLine($"Payment: {payment}"); } ``` ### Javascript (Wasm) ```typescript // Each recipient is a Spark address or a Spark invoice. An invoice that // names its own token and amount needs neither here. const recipients: BatchRecipient[] = [ { paymentRequest: '', amount: BigInt(1_000), tokenIdentifier: '' }, { paymentRequest: '', amount: undefined, tokenIdentifier: undefined } ] const prepareResponse = await sdk.prepareSendBatch({ recipients }) // Show what the batch debits, one entry per token for (const total of prepareResponse.totals) { // Unset would mean sats, which a batch cannot send yet console.log(`Token ID: ${total.tokenIdentifier}`) console.log(`Total: ${total.amount} token base units`) } // If the totals are acceptable, send the batch const sendResponse = await sdk.sendBatch({ prepareResponse }) // One payment per recipient, in the order they were requested for (const payment of sendResponse.payments) { console.log(`Payment: ${JSON.stringify(payment)}`) } ``` ### React Native ```typescript // Each recipient is a Spark address or a Spark invoice. An invoice that // names its own token and amount needs neither here. const recipients: BatchRecipient[] = [ { paymentRequest: '', amount: BigInt(1_000), tokenIdentifier: '' }, { paymentRequest: '', amount: undefined, tokenIdentifier: undefined } ] const prepareResponse = await sdk.prepareSendBatch({ recipients }) // Show what the batch debits, one entry per token for (const total of prepareResponse.totals) { // Unset would mean sats, which a batch cannot send yet console.log(`Token ID: ${total.tokenIdentifier}`) console.log(`Total: ${total.amount} token base units`) } // If the totals are acceptable, send the batch const sendResponse = await sdk.sendBatch({ prepareResponse }) // One payment per recipient, in the order they were requested for (const payment of sendResponse.payments) { console.log(`Payment: ${JSON.stringify(payment)}`) } ``` ### Flutter ```dart // Each recipient is a Spark address or a Spark invoice. An invoice that // names its own token and amount needs neither here. final recipients = [ BatchRecipient( paymentRequest: '', amount: BigInt.from(1000), tokenIdentifier: '', ), BatchRecipient( paymentRequest: '', amount: null, tokenIdentifier: null, ), ]; final prepareResponse = await sdk.prepareSendBatch( request: PrepareSendBatchRequest(recipients: recipients), ); // Show what the batch debits, one entry per token for (final total in prepareResponse.totals) { // Unset would mean sats, which a batch cannot send yet print('Token ID: ${total.tokenIdentifier}'); print('Total: ${total.amount} token base units'); } // If the totals are acceptable, send the batch final sendResponse = await sdk.sendBatch( request: SendBatchRequest(prepareResponse: prepareResponse), ); // One payment per recipient, in the order they were requested for (final payment in sendResponse.payments) { print('Payment: $payment'); } ``` ### Python ```python try: # Each recipient is a Spark address or a Spark invoice. An invoice that # names its own token and amount needs neither here. recipients = [ BatchRecipient( payment_request="", amount=1_000, token_identifier="", ), BatchRecipient( payment_request="", amount=None, token_identifier=None, ), ] prepare_response = await sdk.prepare_send_batch( request=PrepareSendBatchRequest(recipients=recipients) ) # Show what the batch debits, one entry per token for total in prepare_response.totals: # Unset would mean sats, which a batch cannot send yet print(f"Token ID: {total.token_identifier}") print(f"Total: {total.amount} token base units") # If the totals are acceptable, send the batch send_response = await sdk.send_batch( request=SendBatchRequest(prepare_response=prepare_response) ) # One payment per recipient, in the order they were requested for payment in send_response.payments: print(f"Payment: {payment}") except Exception as error: logging.error(error) raise ``` ### Go ```go // Each recipient is a Spark address or a Spark invoice. An invoice that // names its own token and amount needs neither here. amount := new(big.Int).SetInt64(1_000) tokenIdentifier := "" recipients := []breez_sdk_spark.BatchRecipient{ { PaymentRequest: "", Amount: &amount, TokenIdentifier: &tokenIdentifier, }, { PaymentRequest: "", Amount: nil, TokenIdentifier: nil, }, } prepareResponse, err := sdk.PrepareSendBatch(breez_sdk_spark.PrepareSendBatchRequest{ Recipients: recipients, }) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } // Show what the batch debits, one entry per token for _, total := range prepareResponse.Totals { // Unset would mean sats, which a batch cannot send yet tokenID := "" if total.TokenIdentifier != nil { tokenID = *total.TokenIdentifier } log.Printf("Token ID: %s", tokenID) log.Printf("Total: %v token base units", total.Amount) } // If the totals are acceptable, send the batch sendResponse, err := sdk.SendBatch(breez_sdk_spark.SendBatchRequest{ PrepareResponse: prepareResponse, }) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } // One payment per recipient, in the order they were requested for _, payment := range sendResponse.Payments { log.Printf("Payment: %#v", payment) } ``` ## Sending the batch API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.send_batch Sending returns one payment per recipient, in the order the recipients were requested. Every payment carries the same transaction hash, so your application can group them later by filtering the payment list on that hash. See [Listing payments](/guide/list_payments.md). Each payment also raises its own `SdkEvent::PaymentSucceeded` event, the same as a single-recipient send. **Developer note** A token send carries no idempotency key. If a batch fails in a way that leaves the outcome unknown, look for the transaction first (filtering the payment list by transaction hash) instead of sending it again, which would pay every recipient twice. ## External signing A batch can also be signed outside the SDK. The flow matches the single-recipient one described in [Client signing](/guide/client_signing.md), with two differences: the unsigned package is built from the batch prepare response, and publishing it returns every payment rather than one. The package carries the same per-asset totals, so the signer can show the user what they are approving. --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Converting tokens](/guide/token_conversion.md)** # Converting tokens Token conversion enables payments to be made without holding the required asset by converting on-the-fly between Bitcoin and tokens using the Flashnet protocol. ## Fetching conversion limits API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.fetch_conversion_limits Before performing a conversion, you can fetch the minimum amounts required for the conversion. The limits depend on the conversion direction: - **Bitcoin to token**: Minimum Bitcoin amount (in satoshis) and minimum token amount to receive (in token base units) - **Token to Bitcoin**: Minimum token amount (in token base units) and minimum Bitcoin amount to receive (in satoshis) ### Rust ```rust // Fetch limits for converting Bitcoin to a token let response = sdk .fetch_conversion_limits(FetchConversionLimitsRequest { conversion_type: ConversionType::FromBitcoin, token_identifier: Some("".to_string()), }) .await?; if let Some(min_from) = response.min_from_amount { info!("Minimum BTC to convert: {} sats", min_from); } if let Some(min_to) = response.min_to_amount { info!("Minimum tokens to receive: {} base units", min_to); } // Fetch limits for converting a token to Bitcoin let response = sdk .fetch_conversion_limits(FetchConversionLimitsRequest { conversion_type: ConversionType::ToBitcoin { from_token_identifier: "".to_string(), }, token_identifier: None, }) .await?; if let Some(min_from) = response.min_from_amount { info!("Minimum tokens to convert: {} base units", min_from); } if let Some(min_to) = response.min_to_amount { info!("Minimum BTC to receive: {} sats", min_to); } ``` ### Swift ```swift // Fetch limits for converting Bitcoin to a token let fromBitcoinResponse = try await sdk.fetchConversionLimits( request: FetchConversionLimitsRequest( conversionType: ConversionType.fromBitcoin, tokenIdentifier: "" )) if let minFrom = fromBitcoinResponse.minFromAmount { print("Minimum BTC to convert: \(minFrom) sats") } if let minTo = fromBitcoinResponse.minToAmount { print("Minimum tokens to receive: \(minTo) base units") } // Fetch limits for converting a token to Bitcoin let toBitcoinResponse = try await sdk.fetchConversionLimits( request: FetchConversionLimitsRequest( conversionType: ConversionType.toBitcoin( fromTokenIdentifier: "" ), tokenIdentifier: nil )) if let minFrom = toBitcoinResponse.minFromAmount { print("Minimum tokens to convert: \(minFrom) base units") } if let minTo = toBitcoinResponse.minToAmount { print("Minimum BTC to receive: \(minTo) sats") } ``` ### Kotlin ```kotlin try { // Fetch limits for converting Bitcoin to a token val fromBitcoinResponse = sdk.fetchConversionLimits( FetchConversionLimitsRequest( conversionType = ConversionType.FromBitcoin, tokenIdentifier = "" ) ) if (fromBitcoinResponse.minFromAmount != null) { println("Minimum BTC to convert: ${fromBitcoinResponse.minFromAmount} sats") } if (fromBitcoinResponse.minToAmount != null) { println("Minimum tokens to receive: ${fromBitcoinResponse.minToAmount} base units") } // Fetch limits for converting a token to Bitcoin val toBitcoinResponse = sdk.fetchConversionLimits( FetchConversionLimitsRequest( conversionType = ConversionType.ToBitcoin( fromTokenIdentifier = "" ), tokenIdentifier = null ) ) if (toBitcoinResponse.minFromAmount != null) { println("Minimum tokens to convert: ${toBitcoinResponse.minFromAmount} base units") } if (toBitcoinResponse.minToAmount != null) { println("Minimum BTC to receive: ${toBitcoinResponse.minToAmount} sats") } } catch (e: Exception) { // handle error } ``` ### C# ```csharp // Fetch limits for converting Bitcoin to a token var fromBitcoinResponse = await sdk.FetchConversionLimits( request: new FetchConversionLimitsRequest( conversionType: new ConversionType.FromBitcoin(), tokenIdentifier: "" ) ); if (fromBitcoinResponse.minFromAmount != null) { Console.WriteLine($"Minimum BTC to convert: {fromBitcoinResponse.minFromAmount} sats"); } if (fromBitcoinResponse.minToAmount != null) { Console.WriteLine($"Minimum tokens to receive: {fromBitcoinResponse.minToAmount} base units"); } // Fetch limits for converting a token to Bitcoin var toBitcoinResponse = await sdk.FetchConversionLimits( request: new FetchConversionLimitsRequest( conversionType: new ConversionType.ToBitcoin( fromTokenIdentifier: "" ), tokenIdentifier: null ) ); if (toBitcoinResponse.minFromAmount != null) { Console.WriteLine($"Minimum tokens to convert: {toBitcoinResponse.minFromAmount} base units"); } if (toBitcoinResponse.minToAmount != null) { Console.WriteLine($"Minimum BTC to receive: {toBitcoinResponse.minToAmount} sats"); } ``` ### Javascript (Wasm) ```typescript // Fetch limits for converting Bitcoin to a token const fromBitcoinResponse = await sdk.fetchConversionLimits({ conversionType: { type: 'fromBitcoin' }, tokenIdentifier: '' }) if (fromBitcoinResponse.minFromAmount !== undefined) { console.log(`Minimum BTC to convert: ${fromBitcoinResponse.minFromAmount} sats`) } if (fromBitcoinResponse.minToAmount !== undefined) { console.log(`Minimum tokens to receive: ${fromBitcoinResponse.minToAmount} base units`) } // Fetch limits for converting a token to Bitcoin const toBitcoinResponse = await sdk.fetchConversionLimits({ conversionType: { type: 'toBitcoin', fromTokenIdentifier: '' }, tokenIdentifier: undefined }) if (toBitcoinResponse.minFromAmount !== undefined) { console.log(`Minimum tokens to convert: ${toBitcoinResponse.minFromAmount} base units`) } if (toBitcoinResponse.minToAmount !== undefined) { console.log(`Minimum BTC to receive: ${toBitcoinResponse.minToAmount} sats`) } ``` ### React Native ```typescript // Fetch limits for converting Bitcoin to a token const fromBitcoinResponse = await sdk.fetchConversionLimits({ conversionType: new ConversionType.FromBitcoin(), tokenIdentifier: '' }) if (fromBitcoinResponse.minFromAmount !== undefined) { console.log(`Minimum BTC to convert: ${fromBitcoinResponse.minFromAmount} sats`) } if (fromBitcoinResponse.minToAmount !== undefined) { console.log(`Minimum tokens to receive: ${fromBitcoinResponse.minToAmount} base units`) } // Fetch limits for converting a token to Bitcoin const toBitcoinResponse = await sdk.fetchConversionLimits({ conversionType: new ConversionType.ToBitcoin({ fromTokenIdentifier: '' }), tokenIdentifier: undefined }) if (toBitcoinResponse.minFromAmount !== undefined) { console.log(`Minimum tokens to convert: ${toBitcoinResponse.minFromAmount} base units`) } if (toBitcoinResponse.minToAmount !== undefined) { console.log(`Minimum BTC to receive: ${toBitcoinResponse.minToAmount} sats`) } ``` ### Flutter ```dart // Fetch limits for converting Bitcoin to a token final fromBitcoinResponse = await sdk.fetchConversionLimits( request: FetchConversionLimitsRequest( conversionType: ConversionType.fromBitcoin(), tokenIdentifier: '', ), ); if (fromBitcoinResponse.minFromAmount != null) { print('Minimum BTC to convert: ${fromBitcoinResponse.minFromAmount} sats'); } if (fromBitcoinResponse.minToAmount != null) { print('Minimum tokens to receive: ${fromBitcoinResponse.minToAmount} base units'); } // Fetch limits for converting a token to Bitcoin final toBitcoinResponse = await sdk.fetchConversionLimits( request: FetchConversionLimitsRequest( conversionType: ConversionType.toBitcoin( fromTokenIdentifier: '', ), tokenIdentifier: null, ), ); if (toBitcoinResponse.minFromAmount != null) { print('Minimum tokens to convert: ${toBitcoinResponse.minFromAmount} base units'); } if (toBitcoinResponse.minToAmount != null) { print('Minimum BTC to receive: ${toBitcoinResponse.minToAmount} sats'); } ``` ### Python ```python try: # Fetch limits for converting Bitcoin to a token from_bitcoin_response = await sdk.fetch_conversion_limits( request=FetchConversionLimitsRequest( conversion_type=ConversionType.FROM_BITCOIN(), token_identifier="", ) ) if from_bitcoin_response.min_from_amount is not None: print(f"Minimum BTC to convert: {from_bitcoin_response.min_from_amount} sats") if from_bitcoin_response.min_to_amount is not None: print(f"Minimum tokens to receive: {from_bitcoin_response.min_to_amount} base units") # Fetch limits for converting a token to Bitcoin to_bitcoin_response = await sdk.fetch_conversion_limits( request=FetchConversionLimitsRequest( conversion_type=ConversionType.TO_BITCOIN( from_token_identifier="" ), token_identifier=None, ) ) if to_bitcoin_response.min_from_amount is not None: print(f"Minimum tokens to convert: {to_bitcoin_response.min_from_amount} base units") if to_bitcoin_response.min_to_amount is not None: print(f"Minimum BTC to receive: {to_bitcoin_response.min_to_amount} sats") except Exception as error: logging.error(error) raise ``` ### Go ```go // Fetch limits for converting Bitcoin to a token tokenIdentifier := "" fromBitcoinResponse, err := sdk.FetchConversionLimits(breez_sdk_spark.FetchConversionLimitsRequest{ ConversionType: breez_sdk_spark.ConversionTypeFromBitcoin{}, TokenIdentifier: &tokenIdentifier, }) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } if fromBitcoinResponse.MinFromAmount != nil { log.Printf("Minimum BTC to convert: %v sats", *fromBitcoinResponse.MinFromAmount) } if fromBitcoinResponse.MinToAmount != nil { log.Printf("Minimum tokens to receive: %v base units", *fromBitcoinResponse.MinToAmount) } // Fetch limits for converting a token to Bitcoin fromTokenIdentifier := "" toBitcoinResponse, err := sdk.FetchConversionLimits(breez_sdk_spark.FetchConversionLimitsRequest{ ConversionType: breez_sdk_spark.ConversionTypeToBitcoin{ FromTokenIdentifier: fromTokenIdentifier, }, TokenIdentifier: nil, }) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } if toBitcoinResponse.MinFromAmount != nil { log.Printf("Minimum tokens to convert: %v base units", *toBitcoinResponse.MinFromAmount) } if toBitcoinResponse.MinToAmount != nil { log.Printf("Minimum BTC to receive: %v sats", *toBitcoinResponse.MinToAmount) } ``` **Developer note** Amounts are denominated in satoshis for Bitcoin (1 BTC = 100,000,000 sats) and in token base units for tokens. Token base units depend on the token's decimal specification. ## Converting Bitcoin to tokens API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.prepare_send_payment Token conversion enables payments of tokens like USDB to be made without holding the token, but instead using Bitcoin. To do so, when preparing to send a payment, set the conversion options. The conversion will first calculate the Bitcoin amount needed to be converted into the token, convert Bitcoin into that token amount, and then finally complete the payment. ### Rust ```rust let payment_request = "".to_string(); // Token identifier must match the invoice in case it specifies one. let token_identifier = Some("".to_string()); // Set the amount of tokens you wish to send (in token base units). let amount = Some(1_000); // Set to use Bitcoin funds to pay via conversion let optional_max_slippage_bps = Some(50); let optional_completion_timeout_secs = Some(30); let conversion_options = Some(ConversionOptions { conversion_type: ConversionType::FromBitcoin, max_slippage_bps: optional_max_slippage_bps, completion_timeout_secs: optional_completion_timeout_secs, }); let prepare_response = sdk .prepare_send_payment(PrepareSendPaymentRequest { payment_request: PaymentRequest::Input { input: payment_request, }, amount, token_identifier, conversion_options, fee_policy: None, }) .await?; // If the fees are acceptable, continue to send the token payment if let Some(conversion_estimate) = &prepare_response.conversion_estimate { info!( "Estimated conversion: {} token units → {} sats", conversion_estimate.amount_in, conversion_estimate.amount_out ); info!( "Estimated conversion fee: {} token units", conversion_estimate.fee ); } ``` ### Swift ```swift let paymentRequest = "" // Token identifier must match the invoice in case it specifies one. let tokenIdentifier: String? = "" // Set the amount of tokens you wish to send. let amount: BInt? = BInt(1_000) // Set to use Bitcoin funds to pay via conversion let optionalMaxSlippageBps = UInt32(50) let optionalCompletionTimeoutSecs = UInt32(30) let conversionOptions = ConversionOptions( conversionType: ConversionType.fromBitcoin, maxSlippageBps: optionalMaxSlippageBps, completionTimeoutSecs: optionalCompletionTimeoutSecs ) let prepareResponse = try await sdk.prepareSendPayment( request: PrepareSendPaymentRequest( paymentRequest: .input(input: paymentRequest), amount: amount, tokenIdentifier: tokenIdentifier, conversionOptions: conversionOptions, feePolicy: nil )) // If the fees are acceptable, continue to send the token payment if let conversionEstimate = prepareResponse.conversionEstimate { print( "Estimated conversion: \(conversionEstimate.amountIn) token units " + "→ \(conversionEstimate.amountOut) sats") print("Estimated conversion fee: \(conversionEstimate.fee) token units") } ``` ### Kotlin ```kotlin try { val paymentRequest = "" // Token identifier must match the invoice in case it specifies one. val tokenIdentifier = "" // Set the amount of tokens you wish to send (in token base units). // Kotlin MPP (BigInteger from com.ionspin.kotlin.bignum.integer) val amount = BigInteger.fromLong(1_000L) // Android (BigInteger from java.math) // val amount = BigInteger.valueOf(1_000L) // Set to use Bitcoin funds to pay via conversion val optionalMaxSlippageBps = 50u val optionalCompletionTimeoutSecs = 30u val conversionOptions = ConversionOptions( conversionType = ConversionType.FromBitcoin, maxSlippageBps = optionalMaxSlippageBps, completionTimeoutSecs = optionalCompletionTimeoutSecs ) val prepareResponse = sdk.prepareSendPayment( PrepareSendPaymentRequest( paymentRequest = PaymentRequest.Input(input = paymentRequest), amount = amount, tokenIdentifier = tokenIdentifier, conversionOptions = conversionOptions, feePolicy = null, ) ) // If the fees are acceptable, continue to send the token payment prepareResponse.conversionEstimate?.let { conversionEstimate -> println( "Estimated conversion: ${conversionEstimate.amountIn} token units " + "→ ${conversionEstimate.amountOut} sats" ) println("Estimated conversion fee: ${conversionEstimate.fee} token units") } } catch (e: Exception) { // handle error } ``` ### C# ```csharp var paymentRequest = ""; // Token identifier must match the invoice in case it specifies one. var tokenIdentifier = ""; // Set the amount of tokens you wish to send. ulong? amount = 1_000UL; // Optionally set to use Bitcoin funds to pay via conversion var optionalMaxSlippageBps = 50U; var optionalCompletionTimeoutSecs = 30U; var conversionOptions = new ConversionOptions( conversionType: new ConversionType.FromBitcoin(), maxSlippageBps: optionalMaxSlippageBps, completionTimeoutSecs: optionalCompletionTimeoutSecs ); var prepareResponse = await sdk.PrepareSendPayment( request: new PrepareSendPaymentRequest( paymentRequest: new PaymentRequest.Input(input: paymentRequest), amount: amount, tokenIdentifier: tokenIdentifier, conversionOptions: conversionOptions, feePolicy: null ) ); // If the fees are acceptable, continue to send the token payment if (prepareResponse.conversionEstimate != null) { Console.WriteLine("Estimated conversion: " + $"{prepareResponse.conversionEstimate.amountIn} token units " + $"→ {prepareResponse.conversionEstimate.amountOut} sats"); Console.WriteLine("Estimated conversion fee: " + $"{prepareResponse.conversionEstimate.fee} token units"); } ``` ### Javascript (Wasm) ```typescript const paymentRequest = '' // Token identifier must match the invoice in case it specifies one. const tokenIdentifier = '' // Set the amount of tokens you wish to send (in token base units). const amount = BigInt(1_000) // Set to use Bitcoin funds to pay via conversion const optionalMaxSlippageBps = 50 const optionalCompletionTimeoutSecs = 30 const conversionOptions: ConversionOptions = { conversionType: { type: 'fromBitcoin' }, maxSlippageBps: optionalMaxSlippageBps, completionTimeoutSecs: optionalCompletionTimeoutSecs } const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: { type: 'input', input: paymentRequest }, amount, tokenIdentifier, conversionOptions, feePolicy: undefined }) // If the fees are acceptable, continue to send the token payment if (prepareResponse.conversionEstimate !== undefined) { const conversionEstimate = prepareResponse.conversionEstimate console.log( `Estimated conversion: ${conversionEstimate.amountIn} token units → ` + `${conversionEstimate.amountOut} sats` ) console.log(`Estimated conversion fee: ${conversionEstimate.fee} token units`) } ``` ### React Native ```typescript const paymentRequest = '' // Token identifier must match the invoice in case it specifies one. const tokenIdentifier = '' // Set the amount of tokens you wish to send (in token base units). const amount = BigInt(1_000) // Set to use Bitcoin funds to pay via conversion const optionalMaxSlippageBps = 50 const optionalCompletionTimeoutSecs = 30 const conversionOptions = { conversionType: new ConversionType.FromBitcoin(), maxSlippageBps: optionalMaxSlippageBps, completionTimeoutSecs: optionalCompletionTimeoutSecs } const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: new PaymentRequest.Input({ input: paymentRequest }), amount, tokenIdentifier, conversionOptions, feePolicy: undefined }) // If the fees are acceptable, continue to send the token payment if (prepareResponse.conversionEstimate !== undefined) { const conversionEstimate = prepareResponse.conversionEstimate console.debug( `Estimated conversion: ${conversionEstimate.amountIn} token units → ` + `${conversionEstimate.amountOut} sats` ) console.debug(`Estimated conversion fee: ${conversionEstimate.fee} token units`) } ``` ### Flutter ```dart final paymentRequest = ''; // Token identifier must match the invoice in case it specifies one. final tokenIdentifier = ''; // Set the amount of tokens you wish to send (in token base units). final amount = BigInt.from(1000); // Set to use Bitcoin funds to pay via conversion int optionalMaxSlippageBps = 50; int optionalCompletionTimeoutSecs = 30; final conversionOptions = ConversionOptions( conversionType: ConversionType.fromBitcoin(), maxSlippageBps: optionalMaxSlippageBps, completionTimeoutSecs: optionalCompletionTimeoutSecs, ); final prepareResponse = await sdk.prepareSendPayment( request: PrepareSendPaymentRequest( paymentRequest: PaymentRequest.input(input: paymentRequest), amount: amount, tokenIdentifier: tokenIdentifier, conversionOptions: conversionOptions, feePolicy: null, ), ); // If the fees are acceptable, continue to send the token payment if (prepareResponse.conversionEstimate != null) { print( "Estimated conversion: ${prepareResponse.conversionEstimate!.amountIn} token units " "→ ${prepareResponse.conversionEstimate!.amountOut} sats"); print( "Estimated conversion fee: ${prepareResponse.conversionEstimate!.fee} token units"); } ``` ### Python ```python try: payment_request = "" token_identifier = "" amount = 1_000 # Set to use Bitcoin funds to pay via conversion optional_max_slippage_bps = 50 optional_completion_timeout_secs = 30 conversion_options = ConversionOptions( conversion_type=ConversionType.FROM_BITCOIN(), max_slippage_bps=optional_max_slippage_bps, completion_timeout_secs=optional_completion_timeout_secs, ) prepare_response = await sdk.prepare_send_payment( request=PrepareSendPaymentRequest( payment_request=PaymentRequest.INPUT(input=payment_request), amount=amount, token_identifier=token_identifier, conversion_options=conversion_options, fee_policy=None, ) ) # If the fees are acceptable, continue to send the token payment if prepare_response.conversion_estimate is not None: conversion_estimate = prepare_response.conversion_estimate logging.debug( f"Estimated conversion: {conversion_estimate.amount_in}" f" token units → {conversion_estimate.amount_out} sats" ) logging.debug( f"Estimated conversion fee: {conversion_estimate.fee} token units" ) except Exception as error: logging.error(error) raise ``` ### Go ```go paymentRequest := "" // Token identifier must match the invoice in case it specifies one. tokenIdentifier := "" // Set the amount of tokens you wish to send. amount := new(big.Int).SetInt64(1_000) // Set to use Bitcoin funds to pay via conversion optionalMaxSlippageBps := uint32(50) optionalCompletionTimeoutSecs := uint32(30) conversionOptions := &breez_sdk_spark.ConversionOptions{ ConversionType: breez_sdk_spark.ConversionTypeToBitcoin{}, MaxSlippageBps: &optionalMaxSlippageBps, CompletionTimeoutSecs: &optionalCompletionTimeoutSecs, } prepareResponse, err := sdk.PrepareSendPayment(breez_sdk_spark.PrepareSendPaymentRequest{ PaymentRequest: breez_sdk_spark.PaymentRequestInput{Input: paymentRequest}, Amount: &amount, TokenIdentifier: &tokenIdentifier, ConversionOptions: conversionOptions, FeePolicy: nil, }) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } // If the fees are acceptable, continue to send the token payment if prepareResponse.ConversionEstimate != nil { log.Printf( "Estimated conversion: %v token units → %v sats", prepareResponse.ConversionEstimate.AmountIn, prepareResponse.ConversionEstimate.AmountOut, ) log.Printf("Estimated conversion fee: %v token units", prepareResponse.ConversionEstimate.Fee) } ``` **Developer note** When a conversion fails due to exceeding the maximum slippage, the conversion will be refunded automatically. **Developer note** The conversion may result in some token balance remaining in the wallet after the payment is sent. This remaining balance is to account for slippage in the conversion. ## Converting tokens to Bitcoin API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.prepare_send_payment Token conversion also enables Bitcoin payments to be made without holding the required Bitcoin, but instead using a supported token asset like USDB. To do so, when preparing to send a payment, set the conversion options. The conversion will first calculate the amount needed to be converted into Bitcoin, convert the token into that Bitcoin amount, and then finally complete the payment. ### Rust ```rust let payment_request = "".to_string(); // Set to use token funds to pay via conversion let optional_max_slippage_bps = Some(50); let optional_completion_timeout_secs = Some(30); let conversion_options = Some(ConversionOptions { conversion_type: ConversionType::ToBitcoin { from_token_identifier: "".to_string(), }, max_slippage_bps: optional_max_slippage_bps, completion_timeout_secs: optional_completion_timeout_secs, }); let prepare_response = sdk .prepare_send_payment(PrepareSendPaymentRequest { payment_request: PaymentRequest::Input { input: payment_request, }, amount: None, token_identifier: None, conversion_options, fee_policy: None, }) .await?; // If the fees are acceptable, continue to create the Send Payment if let Some(conversion_estimate) = &prepare_response.conversion_estimate { info!( "Estimated conversion: {} token units → {} sats", conversion_estimate.amount_in, conversion_estimate.amount_out ); info!( "Estimated conversion fee: {} token units", conversion_estimate.fee ); } ``` ### Swift ```swift let paymentRequest = "" // Set to use token funds to pay via conversion let optionalMaxSlippageBps = UInt32(50) let optionalCompletionTimeoutSecs = UInt32(30) let conversionOptions = ConversionOptions( conversionType: ConversionType.toBitcoin( fromTokenIdentifier: "" ), maxSlippageBps: optionalMaxSlippageBps, completionTimeoutSecs: optionalCompletionTimeoutSecs ) let prepareResponse = try await sdk.prepareSendPayment( request: PrepareSendPaymentRequest( paymentRequest: .input(input: paymentRequest), amount: nil, tokenIdentifier: nil, conversionOptions: conversionOptions, feePolicy: nil )) if let conversionEstimate = prepareResponse.conversionEstimate { print( "Estimated conversion: \(conversionEstimate.amountIn) token units " + "→ \(conversionEstimate.amountOut) sats") print("Estimated conversion fee: \(conversionEstimate.fee) token units") } ``` ### Kotlin ```kotlin val paymentRequest = "" // Set to use token funds to pay via conversion val optionalMaxSlippageBps = 50u val optionalCompletionTimeoutSecs = 30u val conversionOptions = ConversionOptions( conversionType = ConversionType.ToBitcoin( "" ), maxSlippageBps = optionalMaxSlippageBps, completionTimeoutSecs = optionalCompletionTimeoutSecs ) try { val req = PrepareSendPaymentRequest( paymentRequest = PaymentRequest.Input(input = paymentRequest), amount = null, tokenIdentifier = null, conversionOptions = conversionOptions, feePolicy = null, ) val prepareResponse = sdk.prepareSendPayment(req) // If the fees are acceptable, continue to create the Send Payment prepareResponse.conversionEstimate?.let { conversionEstimate -> // Log.v("Breez", "Estimated conversion: ${conversionEstimate.amountIn} // token units → ${conversionEstimate.amountOut} sats") // Log.v("Breez", "Estimated conversion fee: ${conversionEstimate.fee} token units") } } catch (e: Exception) { // handle error } ``` ### C# ```csharp var paymentRequest = ""; // Set to use token funds to pay via conversion var optionalMaxSlippageBps = 50U; var optionalCompletionTimeoutSecs = 30U; var conversionOptions = new ConversionOptions( conversionType: new ConversionType.ToBitcoin( fromTokenIdentifier: "" ), maxSlippageBps: optionalMaxSlippageBps, completionTimeoutSecs: optionalCompletionTimeoutSecs ); var request = new PrepareSendPaymentRequest( paymentRequest: new PaymentRequest.Input(input: paymentRequest), amount: null, tokenIdentifier: null, conversionOptions: conversionOptions, feePolicy: null ); var prepareResponse = await sdk.PrepareSendPayment(request: request); // If the fees are acceptable, continue to create the Send Payment if (prepareResponse.conversionEstimate != null) { Console.WriteLine("Estimated conversion: " + $"{prepareResponse.conversionEstimate.amountIn} token units " + $"→ {prepareResponse.conversionEstimate.amountOut} sats"); Console.WriteLine("Estimated conversion fee: " + $"{prepareResponse.conversionEstimate.fee} token units"); } ``` ### Javascript (Wasm) ```typescript const paymentRequest = '' // Set to use token funds to pay via conversion const optionalMaxSlippageBps = 50 const optionalCompletionTimeoutSecs = 30 const conversionOptions: ConversionOptions = { conversionType: { type: 'toBitcoin', fromTokenIdentifier: '' }, maxSlippageBps: optionalMaxSlippageBps, completionTimeoutSecs: optionalCompletionTimeoutSecs } const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: { type: 'input', input: paymentRequest }, amount: undefined, tokenIdentifier: undefined, conversionOptions, feePolicy: undefined }) // If the fees are acceptable, continue to create the Send Payment if (prepareResponse.conversionEstimate !== undefined) { const conversionEstimate = prepareResponse.conversionEstimate console.debug( `Estimated conversion: ${conversionEstimate.amountIn} token units → ` + `${conversionEstimate.amountOut} sats` ) console.debug(`Estimated conversion fee: ${conversionEstimate.fee} token units`) } ``` ### React Native ```typescript const paymentRequest = '' // Set to use token funds to pay via conversion const optionalMaxSlippageBps = 50 const optionalCompletionTimeoutSecs = 30 const conversionOptions = { conversionType: new ConversionType.ToBitcoin({ fromTokenIdentifier: '' }), maxSlippageBps: optionalMaxSlippageBps, completionTimeoutSecs: optionalCompletionTimeoutSecs } const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: new PaymentRequest.Input({ input: paymentRequest }), amount: undefined, tokenIdentifier: undefined, conversionOptions, feePolicy: undefined }) // If the fees are acceptable, continue to create the Send Payment if (prepareResponse.conversionEstimate !== undefined) { const conversionEstimate = prepareResponse.conversionEstimate console.debug( `Estimated conversion: ${conversionEstimate.amountIn} token units → ` + `${conversionEstimate.amountOut} sats` ) console.debug(`Estimated conversion fee: ${conversionEstimate.fee} token units`) } ``` ### Flutter ```dart String paymentRequest = ""; // Set to use token funds to pay via conversion int optionalMaxSlippageBps = 50; int optionalCompletionTimeoutSecs = 30; final conversionOptions = ConversionOptions( conversionType: ConversionType.toBitcoin( fromTokenIdentifier: "", ), maxSlippageBps: optionalMaxSlippageBps, completionTimeoutSecs: optionalCompletionTimeoutSecs, ); final request = PrepareSendPaymentRequest( paymentRequest: PaymentRequest.input(input: paymentRequest), amount: null, tokenIdentifier: null, conversionOptions: conversionOptions, feePolicy: null); final response = await sdk.prepareSendPayment(request: request); // If the fees are acceptable, continue to create the Send Payment if (response.conversionEstimate != null) { print( "Estimated conversion: ${response.conversionEstimate!.amountIn} token units " "→ ${response.conversionEstimate!.amountOut} sats"); print( "Estimated conversion fee: ${response.conversionEstimate!.fee} token units"); } ``` ### Python ```python payment_request = "" # Set to use token funds to pay via conversion optional_max_slippage_bps = 50 optional_completion_timeout_secs = 30 conversion_options = ConversionOptions( conversion_type=ConversionType.TO_BITCOIN( from_token_identifier="" ), max_slippage_bps=optional_max_slippage_bps, completion_timeout_secs=optional_completion_timeout_secs, ) try: request = PrepareSendPaymentRequest( payment_request=PaymentRequest.INPUT(input=payment_request), amount=None, token_identifier=None, conversion_options=conversion_options, fee_policy=None, ) prepare_response = await sdk.prepare_send_payment(request=request) # If the fees are acceptable, continue to create the Send Payment if prepare_response.conversion_estimate is not None: conversion_estimate = prepare_response.conversion_estimate logging.debug( f"Estimated conversion: {conversion_estimate.amount_in}" f" token units → {conversion_estimate.amount_out} sats" ) logging.debug( f"Estimated conversion fee: {conversion_estimate.fee} token units" ) except Exception as error: logging.error(error) raise ``` ### Go ```go paymentRequest := "" // Set to use token funds to pay via conversion optionalMaxSlippageBps := uint32(50) optionalCompletionTimeoutSecs := uint32(30) conversionOptions := breez_sdk_spark.ConversionOptions{ ConversionType: breez_sdk_spark.ConversionTypeToBitcoin{ FromTokenIdentifier: "", }, MaxSlippageBps: &optionalMaxSlippageBps, CompletionTimeoutSecs: &optionalCompletionTimeoutSecs, } request := breez_sdk_spark.PrepareSendPaymentRequest{ PaymentRequest: breez_sdk_spark.PaymentRequestInput{Input: paymentRequest}, Amount: nil, TokenIdentifier: nil, ConversionOptions: &conversionOptions, FeePolicy: nil, } response, err := sdk.PrepareSendPayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } // If the fees are acceptable, continue to create the Send Payment if response.ConversionEstimate != nil { log.Printf( "Estimated conversion: %v token units → %v sats", response.ConversionEstimate.AmountIn, response.ConversionEstimate.AmountOut, ) log.Printf("Estimated conversion fee: %v token units", response.ConversionEstimate.Fee) } ``` **Developer note** When a conversion fails due to exceeding the maximum slippage, the conversion will be refunded automatically. **Developer note** The conversion may result in some Bitcoin remaining in the wallet after the payment is sent. This remaining Bitcoin is to account for slippage in the conversion. **→ [Issuing tokens](/guide/issuing_tokens.md)** ## Issuing tokens API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.get_token_issuer The Breez SDK provides a specialized Token Issuer interface for managing custom token issuance on the Spark network using the using the [BTKN protocol](https://docs.spark.money/learn/tokens/hello-btkn). This functionality enables token creators to issue, manage, and control their own tokens with advanced features. ### Rust ```rust let token_issuer = sdk.get_token_issuer(); ``` ### Swift ```swift let tokenIssuer = sdk.getTokenIssuer() ``` ### Kotlin ```kotlin val tokenIssuer = sdk.getTokenIssuer() ``` ### C# ```csharp var tokenIssuer = sdk.GetTokenIssuer(); ``` ### Javascript (Wasm) ```typescript const tokenIssuer = sdk.getTokenIssuer() ``` ### React Native ```typescript const tokenIssuer = sdk.getTokenIssuer() ``` ### Flutter ```dart TokenIssuer tokenIssuer = sdk.getTokenIssuer(); ``` ### Python ```python token_issuer = sdk.get_token_issuer() ``` ### Go ```go tokenIssuer := sdk.GetTokenIssuer() ``` ## Token creation API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.TokenIssuer.html#method.create_issuer_token Create a custom token with configurable parameters. Define the decimal precision, max supply and if the token can be frozen. ### Rust ```rust let request = CreateIssuerTokenRequest { name: "My Token".to_string(), ticker: "MTK".to_string(), decimals: 6, is_freezable: false, max_supply: 1_000_000, }; let token_metadata = token_issuer.create_issuer_token(request).await?; info!("Token identifier: {}", token_metadata.identifier); ``` ### Swift ```swift let request = CreateIssuerTokenRequest( name: "My Token", ticker: "MTK", decimals: UInt32(6), isFreezable: false, maxSupply: BInt(1_000_000) ) let tokenMetadata = try await tokenIssuer.createIssuerToken(request: request) print("Token identifier: {}", tokenMetadata.identifier) ``` ### Kotlin ```kotlin try { val request = CreateIssuerTokenRequest( name = "My Token", ticker = "MTK", decimals = 6.toUInt(), isFreezable = false, maxSupply = BigInteger.fromLong(1_000_000L) ) val tokenMetadata = tokenIssuer.createIssuerToken(request) // Log.v("Breez", "Token identifier: ${tokenMetadata.identifier}") } catch (e: Exception) { // Handle exception } ``` ### C# ```csharp var maxSupply = new BigInteger(1000000); var request = new CreateIssuerTokenRequest( name: "My Token", ticker: "MTK", decimals: 6, isFreezable: false, maxSupply: maxSupply ); var tokenMetadata = await tokenIssuer.CreateIssuerToken(request); Console.WriteLine($"Token identifier: {tokenMetadata.identifier}"); ``` ### Javascript (Wasm) ```typescript const tokenMetadata = await tokenIssuer.createIssuerToken({ name: 'My Token', ticker: 'MTK', decimals: 6, isFreezable: false, maxSupply: BigInt(1_000_000) }) console.debug(`Token identifier: ${tokenMetadata.identifier}`) ``` ### React Native ```typescript const tokenMetadata = await tokenIssuer.createIssuerToken({ name: 'My Token', ticker: 'MTK', decimals: 6, isFreezable: false, maxSupply: BigInt(1_000_000) }) console.debug(`Token identifier: ${tokenMetadata.identifier}`) ``` ### Flutter ```dart CreateIssuerTokenRequest request = CreateIssuerTokenRequest( name: "My Token", ticker: "MTK", decimals: 6, isFreezable: false, maxSupply: BigInt.from(1000000), ); TokenMetadata tokenMetadata = await tokenIssuer.createIssuerToken(request: request); print("Token identifier: ${tokenMetadata.identifier}"); ``` ### Python ```python try: request = CreateIssuerTokenRequest( name="My Token", ticker="MTK", decimals=6, is_freezable=False, max_supply=1_000_000, ) token_metadata = await token_issuer.create_issuer_token(request) logging.debug(f"Token identifier: {token_metadata.identifier}") except Exception as error: logging.error(error) raise ``` ### Go ```go request := breez_sdk_spark.CreateIssuerTokenRequest{ Name: "My Token", Ticker: "MTK", Decimals: 6, IsFreezable: false, MaxSupply: new(big.Int).SetInt64(1_000_000), } tokenMetadata, err := tokenIssuer.CreateIssuerToken(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } log.Printf("Token identifier: %v", tokenMetadata.Identifier) ``` ### Creating multiple tokens Token creation is limited to one token per issuer wallet. If you need to create and then manage more than one token using the same mnemonic, we recommend using different account numbers when initializing the SDK. #### Rust ```rust let account_number = 21; let mnemonic = "".to_string(); let seed = Seed::Mnemonic { mnemonic, passphrase: None, }; let config = default_config(Network::Mainnet); let mut builder = SdkBuilder::new(config, seed); builder = builder.with_default_storage("./.data".to_string()); // Set the account number for the SDK builder = builder.with_account_number(account_number); let sdk = builder.build().await?; ``` #### Swift ```swift let accountNumber = UInt32(21) let mnemonic = "" let seed = Seed.mnemonic(mnemonic: mnemonic, passphrase: nil) let config = defaultConfig(network: Network.mainnet) let builder = SdkBuilder(config: config, seed: seed) await builder.withDefaultStorage(storageDir: "./.data") // Set the account number for the SDK await builder.withAccountNumber(accountNumber: accountNumber) let sdk = try await builder.build() ``` #### Kotlin ```kotlin val accountNumber = 21u val mnemonic = "" val seed = Seed.Mnemonic(mnemonic, null) val config = defaultConfig(Network.MAINNET) config.apiKey = "" try { val builder = SdkBuilder(config, seed) builder.withDefaultStorage("./.data") // Set the account number for the SDK builder.withAccountNumber(accountNumber) val sdk = builder.build() } catch (e: Exception) { // handle error } ``` #### C# ```csharp var accountNumber = 21u; var mnemonic = ""; var seed = new Seed.Mnemonic(mnemonic: mnemonic, passphrase: null); var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { apiKey = "" }; var builder = new SdkBuilder(config: config, seed: seed); await builder.WithDefaultStorage(storageDir: "./.data"); // Set the account number for the SDK await builder.WithAccountNumber(accountNumber); var sdk = await builder.Build(); ``` #### Javascript (Wasm) ```typescript await init() const accountNumber = 21 const mnemonic = '' const seed: Seed = { type: 'mnemonic', mnemonic, passphrase: undefined } const config = defaultConfig('mainnet') config.apiKey = '' let builder = SdkBuilder.new(config, seed) builder = await builder.withDefaultStorage('./.data') // Set the account number for the SDK builder = builder.withAccountNumber(accountNumber) const sdk = await builder.build() ``` #### React Native ```typescript const accountNumber = 21 const mnemonic = '' const seed = new Seed.Mnemonic({ mnemonic, passphrase: undefined }) const config = defaultConfig(Network.Mainnet) config.apiKey = '' const builder = new SdkBuilder(config, seed) await builder.withDefaultStorage(`${RNFS.DocumentDirectoryPath}/data`) // Set the account number for the SDK await builder.withAccountNumber(accountNumber) const sdk = await builder.build() ``` #### Flutter ```dart var accountNumber = 21; String mnemonic = ""; final seed = Seed.mnemonic(mnemonic: mnemonic, passphrase: null); final config = defaultConfig(network: Network.mainnet) .copyWith(apiKey: ""); final builder = SdkBuilder(config: config, seed: seed); builder.withDefaultStorage(storageDir: "./.data"); // Set the account number for the SDK builder.withAccountNumber(accountNumber: accountNumber); var sdk = await builder.build(); ``` #### Python ```python account_number = 21 mnemonic = "" seed = Seed.MNEMONIC(mnemonic=mnemonic, passphrase=None) config = default_config(network=Network.MAINNET) config.api_key = "" try: builder = SdkBuilder(config=config, seed=seed) await builder.with_default_storage(storage_dir="./.data") # Set the account number for the SDK await builder.with_account_number(account_number=account_number) sdk = await builder.build() return sdk except Exception as error: logging.error(error) raise ``` #### Go ```go accountNumber := uint32(21) mnemonic := "" var seed breez_sdk_spark.Seed = breez_sdk_spark.SeedMnemonic{ Mnemonic: mnemonic, Passphrase: nil, } apiKey := "" config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) config.ApiKey = &apiKey builder := breez_sdk_spark.NewSdkBuilder(config, seed) builder.WithDefaultStorage("./.data") // Set the account number for the SDK builder.WithAccountNumber(accountNumber) sdk, err := builder.Build() ``` ## Supply Management ### Minting a token API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.TokenIssuer.html#method.mint_issuer_token Mint to increase the circulating supply of the token. #### Rust ```rust let request = MintIssuerTokenRequest { amount: 1_000 }; let payment = token_issuer.mint_issuer_token(request).await?; ``` #### Swift ```swift let request = MintIssuerTokenRequest( amount: BInt(1_000) ) let payment = try await tokenIssuer.mintIssuerToken(request: request) ``` #### Kotlin ```kotlin try { val request = MintIssuerTokenRequest( amount = BigInteger.fromLong(1_000L), ) val payment = tokenIssuer.mintIssuerToken(request) } catch (e: Exception) { // Handle exception } ``` #### C# ```csharp var amount = new BigInteger(1000); var request = new MintIssuerTokenRequest( amount: amount ); var payment = await tokenIssuer.MintIssuerToken(request); ``` #### Javascript (Wasm) ```typescript const payment = await tokenIssuer.mintIssuerToken({ amount: BigInt(1_000) }) ``` #### React Native ```typescript const payment = await tokenIssuer.mintIssuerToken({ amount: BigInt(1_000) }) ``` #### Flutter ```dart MintIssuerTokenRequest request = MintIssuerTokenRequest( amount: BigInt.from(1000), ); Payment payment = await tokenIssuer.mintIssuerToken(request: request); ``` #### Python ```python try: request = MintIssuerTokenRequest( amount=1_000, ) payment = await token_issuer.mint_issuer_token(request) except Exception as error: logging.error(error) raise ``` #### Go ```go request := breez_sdk_spark.MintIssuerTokenRequest{ Amount: new(big.Int).SetInt64(1_000), } payment, err := tokenIssuer.MintIssuerToken(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } ``` ### Burning a token API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.TokenIssuer.html#method.burn_issuer_token Permanently remove tokens from the circulating supply by burning them. #### Rust ```rust let request = BurnIssuerTokenRequest { amount: 1_000 }; let payment = token_issuer.burn_issuer_token(request).await?; ``` #### Swift ```swift let request = BurnIssuerTokenRequest( amount: BInt(1_000) ) let payment = try await tokenIssuer.burnIssuerToken(request: request) ``` #### Kotlin ```kotlin try { val request = BurnIssuerTokenRequest( amount = BigInteger.fromLong(1_000L), ) val payment = tokenIssuer.burnIssuerToken(request) } catch (e: Exception) { // Handle exception } ``` #### C# ```csharp var amount = new BigInteger(1000); var request = new BurnIssuerTokenRequest( amount: amount ); var payment = await tokenIssuer.BurnIssuerToken(request); ``` #### Javascript (Wasm) ```typescript const payment = await tokenIssuer.burnIssuerToken({ amount: BigInt(1_000) }) ``` #### React Native ```typescript const payment = await tokenIssuer.burnIssuerToken({ amount: BigInt(1_000) }) ``` #### Flutter ```dart BurnIssuerTokenRequest request = BurnIssuerTokenRequest( amount: BigInt.from(1000), ); Payment payment = await tokenIssuer.burnIssuerToken(request: request); ``` #### Python ```python try: request = BurnIssuerTokenRequest( amount=1_000, ) payment = await token_issuer.burn_issuer_token(request) except Exception as error: logging.error(error) raise ``` #### Go ```go request := breez_sdk_spark.BurnIssuerTokenRequest{ Amount: new(big.Int).SetInt64(1_000), } payment, err := tokenIssuer.BurnIssuerToken(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } ``` ### Listing mint or burn payments Mint or burn payments are included in the regular payment history that is obtained when [Listing payments](/guide/list_payments.md). You can filter by token transaction type to only include mint, burn or transfer payments. Transfer payments are regular token payments that are not mint or burn payments. #### Rust ```rust // Provide one or multiple of the following filters to // the `payment_details_filter` field when listing payments let payment_details_transfer_filter = PaymentDetailsFilter::Token { tx_type: Some(TokenTransactionType::Transfer), tx_hash: None, conversion_refund_needed: None, }; let payment_details_mint_filter = PaymentDetailsFilter::Token { tx_type: Some(TokenTransactionType::Mint), tx_hash: None, conversion_refund_needed: None, }; let payment_details_burn_filter = PaymentDetailsFilter::Token { tx_type: Some(TokenTransactionType::Burn), tx_hash: None, conversion_refund_needed: None, }; ``` #### Swift ```swift // Provide one or multiple of the following filters to // the `paymentDetailsFilter` field when listing payments let paymentDetailsTransferFilter = PaymentDetailsFilter.token( conversionRefundNeeded: nil, txHash: nil, txType: TokenTransactionType.transfer) let paymentDetailsMintFilter = PaymentDetailsFilter.token( conversionRefundNeeded: nil, txHash: nil, txType: TokenTransactionType.mint) let paymentDetailsBurnFilter = PaymentDetailsFilter.token( conversionRefundNeeded: nil, txHash: nil, txType: TokenTransactionType.burn) ``` #### Kotlin ```kotlin // Provide one or multiple of the following filters to // the `paymentDetailsFilter` field when listing payments val paymentDetailsTransferFilter = PaymentDetailsFilter.Token( txType = TokenTransactionType.TRANSFER, txHash = null, conversionRefundNeeded = null ) val paymentDetailsMintFilter = PaymentDetailsFilter.Token( txType = TokenTransactionType.MINT, txHash = null, conversionRefundNeeded = null ) val paymentDetailsBurnFilter = PaymentDetailsFilter.Token( txType = TokenTransactionType.BURN, txHash = null, conversionRefundNeeded = null ) ``` #### C# ```csharp // Provide one or multiple of the following filters to // the `paymentDetailsFilter` field when listing payments var paymentDefailsTransferFilter = new PaymentDetailsFilter.Token( txType: TokenTransactionType.Transfer, txHash: null, conversionRefundNeeded: null ); var paymentDefailsMintFilter = new PaymentDetailsFilter.Token( txType: TokenTransactionType.Mint, txHash: null, conversionRefundNeeded: null ); var paymentDefailsBurnFilter = new PaymentDetailsFilter.Token( txType: TokenTransactionType.Burn, txHash: null, conversionRefundNeeded: null ); ``` #### Javascript (Wasm) ```typescript // Provide one or multiple of the following filters to // the `paymentDetailsFilter` field when listing payments const paymentDetailsTransferFilter = { type: 'token', txType: 'transfer' } const paymentDetailsMintFilter = { type: 'token', txType: 'mint' } const paymentDetailsBurnFilter = { type: 'token', txType: 'burn' } ``` #### React Native ```typescript // Provide one or multiple of the following filters to // the `paymentDetailsFilter` field when listing payments const paymentDetailsTransferFilter = new PaymentDetailsFilter.Token({ txType: TokenTransactionType.Transfer, txHash: undefined, conversionRefundNeeded: undefined }) const paymentDetailsMintFilter = new PaymentDetailsFilter.Token({ txType: TokenTransactionType.Mint, txHash: undefined, conversionRefundNeeded: undefined }) const paymentDetailsBurnFilter = new PaymentDetailsFilter.Token({ txType: TokenTransactionType.Burn, txHash: undefined, conversionRefundNeeded: undefined }) ``` #### Flutter ```dart // Provide one or multiple of the following filters to // the `paymentDetailsFilter` field when listing payments PaymentDetailsFilter paymentDetailsTransferFilter = PaymentDetailsFilter.token(txType: TokenTransactionType.transfer); PaymentDetailsFilter paymentDetailsMintFilter = PaymentDetailsFilter.token(txType: TokenTransactionType.mint); PaymentDetailsFilter paymentDetailsBurnFilter = PaymentDetailsFilter.token(txType: TokenTransactionType.burn); ``` #### Python ```python # Provide one or multiple of the following filters to # the `payment_details_filter` field when listing payments payment_details_transfer_filter = PaymentDetailsFilter.TOKEN( tx_type=TokenTransactionType.TRANSFER, tx_hash=None, conversion_refund_needed=None ) payment_details_mint_filter = PaymentDetailsFilter.TOKEN( tx_type=TokenTransactionType.MINT, tx_hash=None, conversion_refund_needed=None ) payment_details_burn_filter = PaymentDetailsFilter.TOKEN( tx_type=TokenTransactionType.BURN, tx_hash=None, conversion_refund_needed=None ) ``` #### Go ```go // Provide one or multiple of the following filters to // the `PaymentDetailsFilter` field when listing payments transferType := breez_sdk_spark.TokenTransactionTypeTransfer mintType := breez_sdk_spark.TokenTransactionTypeMint burnType := breez_sdk_spark.TokenTransactionTypeBurn paymentDetailsTransferFilter := breez_sdk_spark.PaymentDetailsFilterToken{TxType: &transferType} paymentDetailsMintFilter := breez_sdk_spark.PaymentDetailsFilterToken{TxType: &mintType} paymentDetailsBurnFilter := breez_sdk_spark.PaymentDetailsFilterToken{TxType: &burnType} ``` ## Query balance & metadata Retrieve the current issued token balance and fetch the token metadata. ### Rust ```rust let token_balance = token_issuer.get_issuer_token_balance().await?; info!("Token balance: {}", token_balance.balance); let token_metadata = token_issuer.get_issuer_token_metadata().await?; info!("Token ticker: {}", token_metadata.ticker); ``` ### Swift ```swift let tokenBalance = try await tokenIssuer.getIssuerTokenBalance() print("Token balance: {}", tokenBalance.balance) let tokenMetadata = try await tokenIssuer.getIssuerTokenMetadata() print("Token ticker: {}", tokenMetadata.ticker) ``` ### Kotlin ```kotlin try { val tokenBalance = tokenIssuer.getIssuerTokenBalance() // Log.v("Breez", "Token balance: ${tokenBalance.balance}") val tokenMetadata = tokenIssuer.getIssuerTokenMetadata() // Log.v("Breez", "Token ticker: ${tokenMetadata.ticker}") } catch (e: Exception) { // Handle exception } ``` ### C# ```csharp var tokenBalance = await tokenIssuer.GetIssuerTokenBalance(); Console.WriteLine($"Token balance: {tokenBalance.balance}"); var tokenMetadata = await tokenIssuer.GetIssuerTokenMetadata(); Console.WriteLine($"Token ticker: {tokenMetadata.ticker}"); ``` ### Javascript (Wasm) ```typescript const tokenBalance = await tokenIssuer.getIssuerTokenBalance() console.debug(`Token balance: ${tokenBalance.balance}`) const tokenMetadata = await tokenIssuer.getIssuerTokenMetadata() console.debug(`Token ticker: ${tokenMetadata.ticker}`) ``` ### React Native ```typescript const tokenBalance = await tokenIssuer.getIssuerTokenBalance() console.debug(`Token balance: ${tokenBalance.balance}`) const tokenMetadata = await tokenIssuer.getIssuerTokenMetadata() console.debug(`Token ticker: ${tokenMetadata.ticker}`) ``` ### Flutter ```dart TokenBalance tokenBalance = await tokenIssuer.getIssuerTokenBalance(); print("Token balance: ${tokenBalance.balance}"); TokenMetadata tokenMetadata = await tokenIssuer.getIssuerTokenMetadata(); print("Token ticker: ${tokenMetadata.ticker}"); ``` ### Python ```python try: token_balance = await token_issuer.get_issuer_token_balance() logging.debug(f"Token balance: {token_balance.balance}") token_metadata = await token_issuer.get_issuer_token_metadata() logging.debug(f"Token ticker: {token_metadata.ticker}") except Exception as error: logging.error(error) raise ``` ### Go ```go tokenBalance, err := tokenIssuer.GetIssuerTokenBalance() if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } log.Printf("Token balance: %v", tokenBalance.Balance) tokenMetadata, err := tokenIssuer.GetIssuerTokenMetadata() if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } log.Printf("Token ticker: %v", tokenMetadata.Ticker) ``` ## Freeze and unfreeze tokens Freeze and unfreeze tokens at a specific Spark address if the token metadata allows it. ### Rust ```rust let spark_address = "".to_string(); // Freeze the tokens held at the specified Spark address let freeze_request = FreezeIssuerTokenRequest { address: spark_address.clone(), }; let freeze_response = token_issuer.freeze_issuer_token(freeze_request).await?; // Unfreeze the tokens held at the specified Spark address let unfreeze_request = UnfreezeIssuerTokenRequest { address: spark_address, }; let unfreeze_response = token_issuer.unfreeze_issuer_token(unfreeze_request).await?; ``` ### Swift ```swift let sparkAddress = "" // Freeze the tokens held at the specified Spark address let freezeRequest = FreezeIssuerTokenRequest( address: sparkAddress ) let freezeResponse = try await tokenIssuer.freezeIssuerToken(request: freezeRequest) // Unfreeze the tokens held at the specified Spark address let unfreezeRequest = UnfreezeIssuerTokenRequest( address: sparkAddress ) let unfreezeResponse = try await tokenIssuer.unfreezeIssuerToken(request: unfreezeRequest) ``` ### Kotlin ```kotlin try { val sparkAddress = "" // Freeze the tokens held at the specified Spark address val freezeRequest = FreezeIssuerTokenRequest( address = sparkAddress, ) val freezeResponse = tokenIssuer.freezeIssuerToken(freezeRequest) // Unfreeze the tokens held at the specified Spark address val unfreezeRequest = UnfreezeIssuerTokenRequest( address = sparkAddress, ) val unfreezeResponse = tokenIssuer.unfreezeIssuerToken(unfreezeRequest) } catch (e: Exception) { // Handle exception } ``` ### C# ```csharp var sparkAddress = ""; var freezeRequest = new FreezeIssuerTokenRequest( address: sparkAddress ); var freezeReponse = await tokenIssuer.FreezeIssuerToken(freezeRequest); var unfreezeRequest = new UnfreezeIssuerTokenRequest( address: sparkAddress ); var unfreezeResponse = await tokenIssuer.UnfreezeIssuerToken(unfreezeRequest); ``` ### Javascript (Wasm) ```typescript const sparkAddress = '' // Freeze the tokens held at the specified Spark address const freezeResponse = await tokenIssuer.freezeIssuerToken({ address: sparkAddress }) // Unfreeze the tokens held at the specified Spark address const unfreezeResponse = await tokenIssuer.unfreezeIssuerToken({ address: sparkAddress }) ``` ### React Native ```typescript const sparkAddress = '' // Freeze the tokens held at the specified Spark address const freezeResponse = await tokenIssuer.freezeIssuerToken({ address: sparkAddress }) // To unfreeze the tokens, use the following: const unfreezeResponse = await tokenIssuer.unfreezeIssuerToken({ address: sparkAddress }) ``` ### Flutter ```dart String sparkAddress = ""; // Freeze the tokens held at the specified Spark address FreezeIssuerTokenRequest freezeRequest = FreezeIssuerTokenRequest(address: sparkAddress); FreezeIssuerTokenResponse freezeResponse = await tokenIssuer.freezeIssuerToken(request: freezeRequest); // Unfreeze the tokens held at the specified Spark address UnfreezeIssuerTokenRequest unfreezeRequest = UnfreezeIssuerTokenRequest(address: sparkAddress); UnfreezeIssuerTokenResponse unfreezeResponse = await tokenIssuer.unfreezeIssuerToken(request: unfreezeRequest); ``` ### Python ```python try: spark_address = "" # Freeze the tokens held at the specified Spark address freeze_request = FreezeIssuerTokenRequest( address=spark_address, ) freeze_response = await token_issuer.freeze_issuer_token(freeze_request) # Unfreeze the tokens held at the specified Spark address unfreeze_request = UnfreezeIssuerTokenRequest( address=spark_address, ) unfreeze_response = await token_issuer.unfreeze_issuer_token(unfreeze_request) except Exception as error: logging.error(error) raise ``` ### Go ```go sparkAddress := "" // Freeze the tokens held at the specified Spark address freezeRequest := breez_sdk_spark.FreezeIssuerTokenRequest{ Address: sparkAddress, } freezeResponse, err := tokenIssuer.FreezeIssuerToken(freezeRequest) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } // Unfreeze the tokens held at the specified Spark address unfreezeRequest := breez_sdk_spark.UnfreezeIssuerTokenRequest{ Address: sparkAddress, } unfreezeResponse, err := tokenIssuer.UnfreezeIssuerToken(unfreezeRequest) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } ``` **→ [Advanced features](/guide/advanced.md)** # Advanced features The SDK supports advanced features that may be useful in specific use cases: - **[Custom configuration](/guide/config.md)** enables fine-tuning the SDK behavior with various configuration options - **[Custom leaf optimization](/guide/optimize.md)** allows defining the leaf optimization policy and controlling when it occurs in order to minimize payment latency - **[Conditional payments](/guide/htlcs.md)** are useful for implementing atomic cross-chain swaps - **[Using an External Signer](/guide/external_signer.md)** provides custom signing logic and enables integrating with hardware wallets, MPC protocols, or existing wallet infrastructure - **[Server mode](/guide/server_mode.md)** is the SDK profile for multi-tenant server deployments where each request builds an ephemeral SDK and the host orchestrates sync, claiming, and event delivery explicitly - **[Client signing](/guide/client_signing.md)** lets a server drive payments while the key that approves them stays with the user, who reviews and signs each payment on their side - **[Using Turnkey](/guide/turnkey.md)** runs the wallet's signing inside a Turnkey secure enclave, so a server can operate wallets without holding key material **→ [Custom configuration](/guide/config.md)** # Custom configuration API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.Config.html The SDK supports various configuration options to customize its behavior. During [initialization](/guide/initializing.md#basic-initialization), you must provide a configuration object, which we recommend creating by modifying the default configuration. This page describes the available configuration options. ## Max deposit claim fee Receiving Bitcoin payments through on-chain deposits may involve fees. This configuration option controls the automatic claiming of incoming funds, allowing it when the required fees are below specified thresholds. The available options are: - Absolute fee amount in sats - Feerate in sats/vbyte - Fastest network recommended fee at the time of claim, with a leeway in sats/vbyte You can also disable automatic claiming entirely. Deposits that are not automatically claimed require manual intervention. By default, automatic claiming is enabled with a maximum feerate of 1 sats/vbyte. This ceiling also caps what the Spark Service Provider may take to credit a deposit [before it matures](/guide/onchain_claims.md#claiming-before-maturity), so raising it both tolerates higher on-chain fees and allows deposits to be claimed sooner. The default is far below any such spread, so deposits wait for maturity until it is raised. That spread grows with the deposit, so a ceiling generous enough to claim a small deposit early may still leave a large one waiting. More information can be found in the [Claiming on-chain deposits](/guide/onchain_claims.md) page. ### Rust ```rust // Create the default config let mut config = default_config(Network::Mainnet); config.api_key = Some("".to_string()); // Disable automatic claiming config.max_deposit_claim_fee = None; // Set a maximum feerate of 10 sat/vB config.max_deposit_claim_fee = Some(MaxFee::Rate { sat_per_vbyte: 10 }); // Set a maximum fee of 1000 sat config.max_deposit_claim_fee = Some(MaxFee::Fixed { amount: 1000 }); // Set the maximum fee to the fastest network recommended fee at the time of claim // with a leeway of 1 sats/vbyte config.max_deposit_claim_fee = Some(MaxFee::NetworkRecommended { leeway_sat_per_vbyte: 1, }); ``` ### Swift ```swift // Create the default config var config = defaultConfig(network: Network.mainnet) config.apiKey = "" // Disable automatic claiming config.maxDepositClaimFee = nil // Set a maximum feerate of 10 sat/vB config.maxDepositClaimFee = MaxFee.rate(satPerVbyte: 10) // Set a maximum fee of 1000 sat config.maxDepositClaimFee = MaxFee.fixed(amount: 1000) // Set the maximum fee to the fastest network recommended fee at the time of claim // with a leeway of 1 sats/vbyte config.maxDepositClaimFee = MaxFee.networkRecommended(leewaySatPerVbyte: 1) ``` ### Kotlin ```kotlin // Create the default config val config = defaultConfig(Network.MAINNET) config.apiKey = "" // Disable automatic claiming config.maxDepositClaimFee = null // Set a maximum feerate of 10 sat/vB config.maxDepositClaimFee = MaxFee.Rate(10u) // Set a maximum fee of 1000 sat config.maxDepositClaimFee = MaxFee.Fixed(1000u) // Set the maximum fee to the fastest network recommended fee at the time of claim // with a leeway of 1 sats/vbyte config.maxDepositClaimFee = MaxFee.NetworkRecommended(1u) ``` ### C# ```csharp // Create the default config with API key var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { apiKey = "" }; // Disable automatic claiming config = config with { maxDepositClaimFee = null }; // Set a maximum feerate of 10 sat/vB config = config with { maxDepositClaimFee = new MaxFee.Rate(satPerVbyte: 10) }; // Set a maximum fee of 1000 sat config = config with { maxDepositClaimFee = new MaxFee.Fixed(amount: 1000) }; // Set the maximum fee to the fastest network recommended fee at the time of claim // with a leeway of 1 sats/vbyte config = config with { maxDepositClaimFee = new MaxFee.NetworkRecommended(leewaySatPerVbyte: 1) }; ``` ### Javascript (Wasm) ```typescript // Create the default config const config = defaultConfig('mainnet') config.apiKey = '' // Disable automatic claiming config.maxDepositClaimFee = undefined // Set a maximum feerate of 10 sat/vB config.maxDepositClaimFee = { type: 'rate', satPerVbyte: 10 } // Set a maximum fee of 1000 sat config.maxDepositClaimFee = { type: 'fixed', amount: 1000 } // Set the maximum fee to the fastest network recommended fee at the time of claim // with a leeway of 1 sats/vbyte config.maxDepositClaimFee = { type: 'networkRecommended', leewaySatPerVbyte: 1 } ``` ### React Native ```typescript // Create the default config const config = defaultConfig(Network.Mainnet) config.apiKey = '' // Disable automatic claiming config.maxDepositClaimFee = undefined // Set a maximum feerate of 10 sat/vB config.maxDepositClaimFee = new MaxFee.Rate({ satPerVbyte: BigInt(10) }) // Set a maximum fee of 1000 sat config.maxDepositClaimFee = new MaxFee.Fixed({ amount: BigInt(1000) }) // Set the maximum fee to the fastest network recommended fee at the time of claim // with a leeway of 1 sats/vbyte config.maxDepositClaimFee = new MaxFee.NetworkRecommended({ leewaySatPerVbyte: BigInt(1) }) ``` ### Flutter ```dart // Create the default config var config = defaultConfig(network: Network.mainnet) .copyWith(apiKey: ""); // Disable automatic claiming config = config.copyWith(maxDepositClaimFee: null); // Set a maximum feerate of 10 sat/vB config = config.copyWith( maxDepositClaimFee: MaxFee.rate(satPerVbyte: BigInt.from(10))); // Set a maximum fee of 1000 sat config = config.copyWith( maxDepositClaimFee: MaxFee.fixed(amount: BigInt.from(1000))); // Set the maximum fee to the fastest network recommended fee at the time of claim // with a leeway of 1 sats/vbyte config = config.copyWith( maxDepositClaimFee: MaxFee.networkRecommended(leewaySatPerVbyte: BigInt.from(1))); ``` ### Python ```python # Create the default config config = default_config(network=Network.MAINNET) config.api_key = "" # Disable automatic claiming config.max_deposit_claim_fee = None # Set a maximum feerate of 10 sat/vB config.max_deposit_claim_fee = MaxFee.RATE(sat_per_vbyte=10) # Set a maximum fee of 1000 sat config.max_deposit_claim_fee = MaxFee.FIXED(amount=1000) # Set the maximum fee to the fastest network recommended fee at the time of claim # with a leeway of 1 sats/vbyte config.max_deposit_claim_fee = MaxFee.NETWORK_RECOMMENDED(leeway_sat_per_vbyte=1) ``` ### Go ```go // Create the default config config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) apiKey := "" config.ApiKey = &apiKey // Disable automatic claiming config.MaxDepositClaimFee = nil // Set a maximum feerate of 10 sat/vB feeRateInterface := breez_sdk_spark.MaxFee(breez_sdk_spark.MaxFeeRate{SatPerVbyte: 10}) config.MaxDepositClaimFee = &feeRateInterface // Set a maximum fee of 1000 sat feeFixedInterface := breez_sdk_spark.MaxFee(breez_sdk_spark.MaxFeeFixed{Amount: 1000}) config.MaxDepositClaimFee = &feeFixedInterface // Set the maximum fee to the fastest network recommended fee at the time of claim // with a leeway of 1 sats/vbyte networkRecommendedInterface := breez_sdk_spark.MaxFee( breez_sdk_spark.MaxFeeNetworkRecommended{LeewaySatPerVbyte: 1}, ) config.MaxDepositClaimFee = &networkRecommendedInterface ``` ## Synchronization interval The SDK performs regular background synchronization to check for payment status updates. You can configure how often this synchronization occurs. The synchronization process is used to detect some payment status updates that are not detected in real-time through event streams. A shorter synchronization interval provides more responsive detection of payment updates but increases resource usage and may trigger API rate limits. The default interval balances responsiveness with resource efficiency for most use cases. ## Background tasks enabled Master switch for all per-instance background tasks. Defaults to `true`, which is the right choice for mobile and single-instance deployments: the SDK runs its periodic sync, real-time sync client, lightning-address recovery, spark private-mode init, leaf and token-output optimizers, the spark-wallet background processor, the [unilateral exit data](#unilateral-exit-data) collector, and the flashnet conversion refunder. Set to `false` for multi-tenant server deployments where the SDK is built per request and the host orchestrates sync, claiming, and event delivery (typically via webhooks) explicitly. No background work is started; explicit operations such as `sync_wallet`, `claim_deposit`, `list_unclaimed_deposits`, `refund_deposit`, and `refund_pending_conversions` continue to work and are the intended entry points in this mode. The recommended way to opt into server mode is via `default_server_config`, which returns the same `Config` as `default_config` with this flag flipped off. See [Server mode](/guide/server_mode.md) for the full profile, lifecycle pattern, and shared-infrastructure wiring. Configuring this field directly is supported if you build your `Config` another way: ### Rust ```rust // Server-mode profile: equivalent to default_server_config(Network::Mainnet). // Recommended when you build the SDK per request in a multi-tenant server // deployment. See the "Server mode" page for the full profile. let mut config = default_config(Network::Mainnet); config.background_tasks_enabled = false; ``` ### Swift ```swift // Server-mode profile: equivalent to defaultServerConfig(network: .mainnet). // Recommended when you build the SDK per request in a multi-tenant server // deployment. See the "Server mode" page for the full profile. var config = defaultConfig(network: Network.mainnet) config.backgroundTasksEnabled = false ``` ### Kotlin ```kotlin // Server-mode profile: equivalent to defaultServerConfig(Network.MAINNET). // Recommended when you build the SDK per request in a multi-tenant // server deployment. See the "Server mode" page for the full profile. val config = defaultConfig(Network.MAINNET) config.backgroundTasksEnabled = false ``` ### C# ```csharp // Server-mode profile: equivalent to DefaultServerConfig(Network.Mainnet). // Recommended when you build the SDK per request in a multi-tenant // server deployment. See the "Server mode" page for the full profile. var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { backgroundTasksEnabled = false }; ``` ### Javascript (Wasm) ```typescript // Server-mode profile: equivalent to defaultServerConfig('mainnet'). // Recommended when you build the SDK per request in a multi-tenant server // deployment. See the "Server mode" page for the full profile. const config = defaultConfig('mainnet') config.backgroundTasksEnabled = false ``` ### React Native ```typescript // Server-mode profile: equivalent to defaultServerConfig(Network.Mainnet). // Recommended when you build the SDK per request in a multi-tenant server // deployment. See the "Server mode" page for the full profile. const config = defaultConfig(Network.Mainnet) config.backgroundTasksEnabled = false ``` ### Flutter ```dart // Server-mode profile: equivalent to defaultServerConfig(network: Network.mainnet). // Recommended when you build the SDK per request in a multi-tenant server // deployment. See the "Server mode" page for the full profile. final config = defaultConfig(network: Network.mainnet) .copyWith(backgroundTasksEnabled: false); ``` ### Python ```python # Server-mode profile: equivalent to default_server_config(network=Network.MAINNET). # Recommended when you build the SDK per request in a multi-tenant server # deployment. See the "Server mode" page for the full profile. config = default_config(network=Network.MAINNET) config.background_tasks_enabled = False ``` ### Go ```go // Server-mode profile: equivalent to DefaultServerConfig(NetworkMainnet). // Recommended when you build the SDK per request in a multi-tenant server // deployment. See the "Server mode" page for the full profile. config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) config.BackgroundTasksEnabled = false ``` **Developer note** When this flag is `false`, related per-field options whose backing service is gated off must be in their inactive shape: - [`real_time_sync_server_url`](#real-time-sync-server-url) must be `None`. - [`leaf_optimization_config.auto_enabled`](#optimization-configuration) must be `false`. - [`token_optimization_config.auto_enabled`](#optimization-configuration) must be `false`. The SDK rejects builds that leave any of them in their active shape with an invalid-input error. `default_server_config` sets these compatible values automatically. ## LNURL Domain The LNURL domain to be used for receiving LNURL and Lightning address payments. By default, the [Breez LNURL server](https://github.com/breez/spark-sdk/tree/main/crates/breez-sdk/lnurl) instance will be used. You may configure a different domain, or set no domain to disable receiving payments using LNURL. For more information, see [Receiving payments using LNURL-Pay](/guide/receive_lnurl_pay.md). ## Prefer Spark over Lightning When enabled, the SDK will settle Lightning invoice payments using the Spark protocol instead of Lightning where possible. It's not recommended to enable it because of the following trade-offs: - **Sending:** No proof of payment (no Lightning preimage). Invoice metadata (the invoice itself, its description) is not persisted with the payment. - **Receiving:** The SDK [embeds a Spark address](https://docs.spark.money/api-reference/wallet/create-lightning-invoice) in the invoice's fallback field. If the payer uses this Spark address, the received payment cannot be linked back to the invoice. ## External input parsing The SDK's parsing module can be extended by providing external parsers that are used when input is not recognized. Some [default external parsers](/guide/parse.md#default-external-parsers) are provided but can be disabled. You can add new external parsers as described in [Configuring external parsers](/guide/parse.md#configuring-external-parsers). ## Real-time sync server URL The SDK synchronizes user data across different SDK instances using a [real-time synchronization server](https://github.com/breez/data-sync). By default, a Breez instance will be used, but you may configure a different instance by providing its URL, or disable it entirely by providing no URL. ## Private mode enabled by default Configures whether the Spark private mode should be enabled by default. By default, it is enabled. When enabled, the Spark private mode will be enabled on the first initialization of the SDK. If disabled, no changes will be made to the Spark private mode. ### Rust ```rust // Disable Spark private mode by default let mut config = default_config(Network::Mainnet); config.private_enabled_default = false; ``` ### Swift ```swift // Disable Spark private mode by default var config = defaultConfig(network: Network.mainnet) config.privateEnabledDefault = false ``` ### Kotlin ```kotlin // Disable Spark private mode by default val config = defaultConfig(Network.MAINNET) config.privateEnabledDefault = false ``` ### C# ```csharp // Disable Spark private mode by default var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { privateEnabledDefault = false }; ``` ### Javascript (Wasm) ```typescript // Disable Spark private mode by default const config = defaultConfig('mainnet') config.privateEnabledDefault = false ``` ### React Native ```typescript // Disable Spark private mode by default const config = defaultConfig(Network.Mainnet) config.privateEnabledDefault = false ``` ### Flutter ```dart // Disable Spark private mode by default var config = defaultConfig(network: Network.mainnet) .copyWith(privateEnabledDefault: false); ``` ### Python ```python # Disable Spark private mode by default config = default_config(network=Network.MAINNET) config.private_enabled_default = False ``` ### Go ```go // Disable Spark private mode by default config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) config.PrivateEnabledDefault = false ``` **Developer note** This configuration option is only relevant when the SDK is initialized for the first time. To update the user settings after that, or to explicitly disable the Spark private mode, see the [User settings](/guide/user_settings.md) page. ## Unilateral exit data Whether the SDK collects the data a [unilateral exit](/guide/unilateral_exit.md) needs, in the background, as funds arrive. Defaults to `true`. An exit is built from each leaf's chain of pre-signed transactions. Holding that chain locally is what lets an exit be quoted and built when the Spark operators are unreachable, so a leaf is only exitable that way once its chain has been collected. Collection runs after an operation finishes rather than during it, which keeps it off the critical path of a payment. Freshly received funds are therefore briefly not yet exitable, for roughly one round trip to the operators. Set it to `false` if bandwidth matters more than being able to recover funds without the operators. Chains are then only collected when an exit is prepared, which needs the operators reachable at that moment, so a leaf stays un-exitable until one is collected. The flag also governs `sync_wallet`, which waits for a collection pass before it returns rather than leaving it to the background. That makes a sync the way to run the collection at a moment of your choosing, and it is what keeps the data current in [server mode](/guide/server_mode.md), where nothing runs in the background to collect it. A leaf whose chain the operators will not complete stays un-exitable until a later attempt succeeds, so a successful sync means the collection ran, not that every leaf is now exitable. ## Optimization configuration The SDK can automatically optimize both the Spark leaf set and a token's output set. Leaf optimization and token-output optimization are configured independently. ### Leaf optimization Leaf optimization maximizes unilateral exit efficiency or increases payment speed. Fewer, larger leaves allow more funds to be exited unilaterally, while having more leaves enables payments without requiring swaps, improving payment speed. - **Automatic optimization enabled**: whether leaf optimization runs automatically when a payment is sent or received. Enabled by default. - **Multiplicity**: the desired multiplicity for the leaf set. Default value is 1. Setting it to 0 fully optimizes for unilateral exit efficiency. Setting it to a value greater than 0 also optimizes for payment speed, with higher values prioritizing payment speed more aggressively at the cost of higher unilateral exit fees. Values above 5 are intended for high-throughput server environments that require maximum TPS and are not recommended for end-user wallets. See [Custom leaf optimization](/guide/optimize.md) for more information and recommendations on how to configure leaf optimization. ### Token-output optimization Token-output optimization automatically consolidates a token's available outputs to keep the output set small while preserving enough distinct outputs for concurrent sends. - **Automatic optimization enabled**: whether token-output consolidation runs automatically. Enabled by default. - **Target output count**: the number of token outputs to produce when consolidation fires. Instead of collapsing a token's outputs into a single output (which would serialize subsequent sends), the SDK splits the consolidated balance across this many outputs of roughly equal value. Higher values preserve concurrency for parallel sends at the cost of a slightly larger output set. Must be at least 1 and strictly less than the minimum outputs threshold. Default value is 5. - **Minimum outputs threshold**: the output count that triggers per-token auto-consolidation. Consolidation runs for a token once its available output count exceeds this value. Must be greater than 1. Default value is 50. #### Rust ```rust let mut config = default_config(Network::Mainnet); config.leaf_optimization_config = LeafOptimizationConfig { auto_enabled: true, multiplicity: 1, }; config.token_optimization_config = TokenOptimizationConfig { auto_enabled: true, target_output_count: 5, min_outputs_threshold: 50, }; ``` #### Swift ```swift var config = defaultConfig(network: Network.mainnet) config.leafOptimizationConfig = LeafOptimizationConfig(autoEnabled: true, multiplicity: 1) config.tokenOptimizationConfig = TokenOptimizationConfig( autoEnabled: true, targetOutputCount: 5, minOutputsThreshold: 50 ) ``` #### Kotlin ```kotlin val config = defaultConfig(Network.MAINNET) config.leafOptimizationConfig = LeafOptimizationConfig( autoEnabled = true, multiplicity = 1u, ) config.tokenOptimizationConfig = TokenOptimizationConfig( autoEnabled = true, targetOutputCount = 5u, minOutputsThreshold = 50u, ) ``` #### C# ```csharp var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { leafOptimizationConfig = new LeafOptimizationConfig( autoEnabled: true, multiplicity: 1 ), tokenOptimizationConfig = new TokenOptimizationConfig( autoEnabled: true, targetOutputCount: 5, minOutputsThreshold: 50 ) }; ``` #### Javascript (Wasm) ```typescript const config = defaultConfig('mainnet') config.leafOptimizationConfig = { autoEnabled: true, multiplicity: 1 } config.tokenOptimizationConfig = { autoEnabled: true, targetOutputCount: 5, minOutputsThreshold: 50 } ``` #### React Native ```typescript const config = defaultConfig(Network.Mainnet) config.leafOptimizationConfig = { autoEnabled: true, multiplicity: 1 } config.tokenOptimizationConfig = { autoEnabled: true, targetOutputCount: 5, minOutputsThreshold: 50 } ``` #### Flutter ```dart var config = defaultConfig(network: Network.mainnet).copyWith( leafOptimizationConfig: LeafOptimizationConfig(autoEnabled: true, multiplicity: 1), tokenOptimizationConfig: TokenOptimizationConfig( autoEnabled: true, targetOutputCount: 5, minOutputsThreshold: 50)); ``` #### Python ```python config = default_config(network=Network.MAINNET) config.leaf_optimization_config = LeafOptimizationConfig( auto_enabled=True, multiplicity=1 ) config.token_optimization_config = TokenOptimizationConfig( auto_enabled=True, target_output_count=5, min_outputs_threshold=50 ) ``` #### Go ```go config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) config.LeafOptimizationConfig = breez_sdk_spark.LeafOptimizationConfig{ AutoEnabled: true, Multiplicity: 1, } config.TokenOptimizationConfig = breez_sdk_spark.TokenOptimizationConfig{ AutoEnabled: true, TargetOutputCount: 5, MinOutputsThreshold: 50, } ``` ## Spark environment configuration The SDK comes pre-configured for the default Spark operator network. For advanced use cases such as connecting to alternative Spark deployments (e.g. dev or staging environments), you can override the operator pool, service provider, threshold, and token withdrawal settings by providing a custom Spark configuration. The configuration requires: - **Signing operators**: The set of Spark operators with their identifiers, gRPC addresses, and identity public keys. - **Coordinator identifier**: Which operator acts as the coordinator. - **Threshold**: The FROST signing threshold (e.g. 2-of-3). - **SSP configuration**: The Service Provider's base URL, identity public key, and optionally a custom GraphQL schema endpoint path. - **Token withdrawal settings**: Expected bond amount and relative block locktime for token withdrawals. ### Rust ```rust let mut config = default_config(Network::Mainnet); // Connect to a custom Spark environment config.spark_config = Some(SparkConfig { coordinator_identifier: "0000000000000000000000000000000000000000000000000000000000000001" .to_string(), threshold: 2, signing_operators: vec![ SparkSigningOperator { id: 0, identifier: "0000000000000000000000000000000000000000000000000000000000000001" .to_string(), address: "https://0.spark.example.com".to_string(), identity_public_key: "03acd9a5a88db102730ff83dee69d69088cc4c9d93bbee893e90fd5051b7da9651".to_string(), ca_cert_pem: None, }, SparkSigningOperator { id: 1, identifier: "0000000000000000000000000000000000000000000000000000000000000002" .to_string(), address: "https://1.spark.example.com".to_string(), identity_public_key: "02d2d103cacb1d6355efeab27637c74484e2a7459e49110c3fe885210369782e23".to_string(), ca_cert_pem: None, }, SparkSigningOperator { id: 2, identifier: "0000000000000000000000000000000000000000000000000000000000000003" .to_string(), address: "https://2.spark.example.com".to_string(), identity_public_key: "0350f07ffc21bfd59d31e0a7a600e2995273938444447cb9bc4c75b8a895dbb853".to_string(), ca_cert_pem: None, }, ], ssp_config: SparkSspConfig { base_url: "https://api.example.com".to_string(), identity_public_key: "02e0b8d42c5d3b5fe4c5beb6ea796ab3bc8aaf28a3d3195407482c67e0b58228a5".to_string(), schema_endpoint: Some("graphql/spark/rc".to_string()), }, expected_withdraw_bond_sats: 10_000, expected_withdraw_relative_block_locktime: 1_000, max_token_transaction_inputs: None, }); ``` ### Kotlin ```kotlin val config = defaultConfig(Network.MAINNET) // Connect to a custom Spark environment config.sparkConfig = SparkConfig( coordinatorIdentifier = "0000000000000000000000000000000000000000000000000000000000000001", threshold = 2u, signingOperators = listOf( SparkSigningOperator( id = 0u, identifier = "0000000000000000000000000000000000000000000000000000000000000001", address = "https://0.spark.example.com", identityPublicKey = "03acd9a5a88db102730ff83dee69d69088cc4c9d93bbee893e90fd5051b7da9651", caCertPem = null ), SparkSigningOperator( id = 1u, identifier = "0000000000000000000000000000000000000000000000000000000000000002", address = "https://1.spark.example.com", identityPublicKey = "02d2d103cacb1d6355efeab27637c74484e2a7459e49110c3fe885210369782e23", caCertPem = null ), SparkSigningOperator( id = 2u, identifier = "0000000000000000000000000000000000000000000000000000000000000003", address = "https://2.spark.example.com", identityPublicKey = "0350f07ffc21bfd59d31e0a7a600e2995273938444447cb9bc4c75b8a895dbb853", caCertPem = null ) ), sspConfig = SparkSspConfig( baseUrl = "https://api.example.com", identityPublicKey = "02e0b8d42c5d3b5fe4c5beb6ea796ab3bc8aaf28a3d3195407482c67e0b58228a5", schemaEndpoint = "graphql/spark/rc" ), expectedWithdrawBondSats = 10_000u, expectedWithdrawRelativeBlockLocktime = 1_000u, maxTokenTransactionInputs = null ) ``` ### C# ```csharp var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { // Connect to a custom Spark environment sparkConfig = new SparkConfig( coordinatorIdentifier: "0000000000000000000000000000000000000000000000000000000000000001", threshold: 2, signingOperators: new[] { new SparkSigningOperator( id: 0, identifier: "0000000000000000000000000000000000000000000000000000000000000001", address: "https://0.spark.example.com", identityPublicKey: "03acd9a5a88db102730ff83dee69d69088cc4c9d93bbee893e90fd5051b7da9651", caCertPem: null ), new SparkSigningOperator( id: 1, identifier: "0000000000000000000000000000000000000000000000000000000000000002", address: "https://1.spark.example.com", identityPublicKey: "02d2d103cacb1d6355efeab27637c74484e2a7459e49110c3fe885210369782e23", caCertPem: null ), new SparkSigningOperator( id: 2, identifier: "0000000000000000000000000000000000000000000000000000000000000003", address: "https://2.spark.example.com", identityPublicKey: "0350f07ffc21bfd59d31e0a7a600e2995273938444447cb9bc4c75b8a895dbb853", caCertPem: null ) }, sspConfig: new SparkSspConfig( baseUrl: "https://api.example.com", identityPublicKey: "02e0b8d42c5d3b5fe4c5beb6ea796ab3bc8aaf28a3d3195407482c67e0b58228a5", schemaEndpoint: "graphql/spark/rc" ), expectedWithdrawBondSats: 10000, expectedWithdrawRelativeBlockLocktime: 1000, maxTokenTransactionInputs: null ) }; ``` ### Javascript (Wasm) ```typescript const config = defaultConfig('mainnet') // Connect to a custom Spark environment config.sparkConfig = { coordinatorIdentifier: '0000000000000000000000000000000000000000000000000000000000000001', threshold: 2, signingOperators: [ { id: 0, identifier: '0000000000000000000000000000000000000000000000000000000000000001', address: 'https://0.spark.example.com', identityPublicKey: '03acd9a5a88db102730ff83dee69d69088cc4c9d93bbee893e90fd5051b7da9651' }, { id: 1, identifier: '0000000000000000000000000000000000000000000000000000000000000002', address: 'https://1.spark.example.com', identityPublicKey: '02d2d103cacb1d6355efeab27637c74484e2a7459e49110c3fe885210369782e23' }, { id: 2, identifier: '0000000000000000000000000000000000000000000000000000000000000003', address: 'https://2.spark.example.com', identityPublicKey: '0350f07ffc21bfd59d31e0a7a600e2995273938444447cb9bc4c75b8a895dbb853' } ], sspConfig: { baseUrl: 'https://api.example.com', identityPublicKey: '02e0b8d42c5d3b5fe4c5beb6ea796ab3bc8aaf28a3d3195407482c67e0b58228a5', schemaEndpoint: 'graphql/spark/rc' }, expectedWithdrawBondSats: 10_000, expectedWithdrawRelativeBlockLocktime: 1_000 } ``` ### React Native ```typescript const config = defaultConfig(Network.Mainnet) // Connect to a custom Spark environment config.sparkConfig = { coordinatorIdentifier: '0000000000000000000000000000000000000000000000000000000000000001', threshold: 2, signingOperators: [ { id: 0, identifier: '0000000000000000000000000000000000000000000000000000000000000001', address: 'https://0.spark.example.com', identityPublicKey: '03acd9a5a88db102730ff83dee69d69088cc4c9d93bbee893e90fd5051b7da9651', caCertPem: undefined }, { id: 1, identifier: '0000000000000000000000000000000000000000000000000000000000000002', address: 'https://1.spark.example.com', identityPublicKey: '02d2d103cacb1d6355efeab27637c74484e2a7459e49110c3fe885210369782e23', caCertPem: undefined }, { id: 2, identifier: '0000000000000000000000000000000000000000000000000000000000000003', address: 'https://2.spark.example.com', identityPublicKey: '0350f07ffc21bfd59d31e0a7a600e2995273938444447cb9bc4c75b8a895dbb853', caCertPem: undefined } ], sspConfig: { baseUrl: 'https://api.example.com', identityPublicKey: '02e0b8d42c5d3b5fe4c5beb6ea796ab3bc8aaf28a3d3195407482c67e0b58228a5', schemaEndpoint: 'graphql/spark/rc' }, expectedWithdrawBondSats: BigInt(10_000), expectedWithdrawRelativeBlockLocktime: BigInt(1_000), maxTokenTransactionInputs: undefined } ``` ### Flutter ```dart var config = defaultConfig(network: Network.mainnet).copyWith( // Connect to a custom Spark environment sparkConfig: SparkConfig( coordinatorIdentifier: '0000000000000000000000000000000000000000000000000000000000000001', threshold: 2, signingOperators: [ SparkSigningOperator( id: 0, identifier: '0000000000000000000000000000000000000000000000000000000000000001', address: 'https://0.spark.example.com', identityPublicKey: '03acd9a5a88db102730ff83dee69d69088cc4c9d93bbee893e90fd5051b7da9651', caCertPem: null), SparkSigningOperator( id: 1, identifier: '0000000000000000000000000000000000000000000000000000000000000002', address: 'https://1.spark.example.com', identityPublicKey: '02d2d103cacb1d6355efeab27637c74484e2a7459e49110c3fe885210369782e23', caCertPem: null), SparkSigningOperator( id: 2, identifier: '0000000000000000000000000000000000000000000000000000000000000003', address: 'https://2.spark.example.com', identityPublicKey: '0350f07ffc21bfd59d31e0a7a600e2995273938444447cb9bc4c75b8a895dbb853', caCertPem: null), ], sspConfig: SparkSspConfig( baseUrl: 'https://api.example.com', identityPublicKey: '02e0b8d42c5d3b5fe4c5beb6ea796ab3bc8aaf28a3d3195407482c67e0b58228a5', schemaEndpoint: 'graphql/spark/rc'), expectedWithdrawBondSats: BigInt.from(10000), expectedWithdrawRelativeBlockLocktime: BigInt.from(1000))); ``` ### Python ```python config = default_config(network=Network.MAINNET) # Connect to a custom Spark environment config.spark_config = SparkConfig( coordinator_identifier="0000000000000000000000000000000000000000000000000000000000000001", threshold=2, signing_operators=[ SparkSigningOperator( id=0, identifier="0000000000000000000000000000000000000000000000000000000000000001", address="https://0.spark.example.com", identity_public_key=( "03acd9a5a88db102730ff83dee69d69088cc4c9d93bbee893e90fd5051b7da9651" ), ca_cert_pem=None, ), SparkSigningOperator( id=1, identifier="0000000000000000000000000000000000000000000000000000000000000002", address="https://1.spark.example.com", identity_public_key=( "02d2d103cacb1d6355efeab27637c74484e2a7459e49110c3fe885210369782e23" ), ca_cert_pem=None, ), SparkSigningOperator( id=2, identifier="0000000000000000000000000000000000000000000000000000000000000003", address="https://2.spark.example.com", identity_public_key=( "0350f07ffc21bfd59d31e0a7a600e2995273938444447cb9bc4c75b8a895dbb853" ), ca_cert_pem=None, ), ], ssp_config=SparkSspConfig( base_url="https://api.example.com", identity_public_key=( "02e0b8d42c5d3b5fe4c5beb6ea796ab3bc8aaf28a3d3195407482c67e0b58228a5" ), schema_endpoint="graphql/spark/rc", ), expected_withdraw_bond_sats=10_000, expected_withdraw_relative_block_locktime=1_000, max_token_transaction_inputs=None, ) ``` ### Go ```go config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) // Connect to a custom Spark environment schemaEndpoint := "graphql/spark/rc" sparkConfig := breez_sdk_spark.SparkConfig{ CoordinatorIdentifier: "0000000000000000000000000000000000000000000000000000000000000001", Threshold: 2, SigningOperators: []breez_sdk_spark.SparkSigningOperator{ { Id: 0, Identifier: "0000000000000000000000000000000000000000000000000000000000000001", Address: "https://0.spark.example.com", IdentityPublicKey: "03acd9a5a88db102730ff83dee69d69088cc4c9d93bbee893e90fd5051b7da9651", CaCertPem: nil, }, { Id: 1, Identifier: "0000000000000000000000000000000000000000000000000000000000000002", Address: "https://1.spark.example.com", IdentityPublicKey: "02d2d103cacb1d6355efeab27637c74484e2a7459e49110c3fe885210369782e23", CaCertPem: nil, }, { Id: 2, Identifier: "0000000000000000000000000000000000000000000000000000000000000003", Address: "https://2.spark.example.com", IdentityPublicKey: "0350f07ffc21bfd59d31e0a7a600e2995273938444447cb9bc4c75b8a895dbb853", CaCertPem: nil, }, }, SspConfig: breez_sdk_spark.SparkSspConfig{ BaseUrl: "https://api.example.com", IdentityPublicKey: "02e0b8d42c5d3b5fe4c5beb6ea796ab3bc8aaf28a3d3195407482c67e0b58228a5", SchemaEndpoint: &schemaEndpoint, }, ExpectedWithdrawBondSats: 10_000, ExpectedWithdrawRelativeBlockLocktime: 1_000, } config.SparkConfig = &sparkConfig ``` **Developer note** This is an advanced configuration option intended for Spark operators and developers working with custom Spark deployments. Most integrators should use the default configuration. ## Maximum concurrent claims Controls how many pending Spark transfers can be claimed in parallel. The default value of 4 provides a good balance between throughput and resource usage for most applications. For server environments or applications that receive a high volume of incoming payments, increasing this value can further improve throughput by processing more claims concurrently. **Default**: 4 **Recommendation**: The default value works well for most applications. Server applications handling many simultaneous incoming payments may benefit from higher values (e.g., 8-16), depending on their infrastructure capacity. End-user wallets with limited resources may reduce this to 1-2. ## Stable balance configuration API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.StableBalanceConfig.html The SDK can convert Bitcoin to a stable token on receive and vice versa on send, protecting against price volatility. Configure the available tokens, default behavior, conversion threshold, and slippage tolerance. See the [Stable balance](/guide/stable_balance.md) guide for full details. ### Rust ```rust let mut config = default_config(Network::Mainnet); // Enable stable balance with USDB conversion config.stable_balance_config = Some(StableBalanceConfig { tokens: vec![StableBalanceToken { label: "USDB".to_string(), token_identifier: "btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87" .to_string(), }], default_active_label: Some("USDB".to_string()), threshold_sats: None, max_slippage_bps: None, }); ``` ### Swift ```swift var config = defaultConfig(network: Network.mainnet) // Enable stable balance with USDB conversion config.stableBalanceConfig = StableBalanceConfig( tokens: [StableBalanceToken( label: "USDB", tokenIdentifier: "btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87" )], defaultActiveLabel: "USDB" ) ``` ### Kotlin ```kotlin val config = defaultConfig(Network.MAINNET) // Enable stable balance with USDB conversion config.stableBalanceConfig = StableBalanceConfig( tokens = listOf(StableBalanceToken( label = "USDB", tokenIdentifier = "btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87", )), defaultActiveLabel = "USDB", ) ``` ### C# ```csharp var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { // Enable stable balance with USDB conversion stableBalanceConfig = new StableBalanceConfig( tokens: new StableBalanceToken[] { new StableBalanceToken( label: "USDB", tokenIdentifier: "btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87" ) }, defaultActiveLabel: "USDB" ) }; ``` ### Javascript (Wasm) ```typescript const config = defaultConfig('mainnet') // Enable stable balance with USDB conversion config.stableBalanceConfig = { tokens: [{ label: 'USDB', tokenIdentifier: 'btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87' }], defaultActiveLabel: 'USDB' } ``` ### React Native ```typescript const config = defaultConfig(Network.Mainnet) // Enable stable balance with USDB conversion config.stableBalanceConfig = { tokens: [{ label: 'USDB', tokenIdentifier: 'btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87' }], defaultActiveLabel: 'USDB', thresholdSats: undefined, maxSlippageBps: undefined } ``` ### Flutter ```dart var config = defaultConfig(network: Network.mainnet).copyWith( // Enable stable balance with USDB conversion stableBalanceConfig: StableBalanceConfig( tokens: [StableBalanceToken( label: "USDB", tokenIdentifier: "btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87", )], defaultActiveLabel: "USDB", )); ``` ### Python ```python config = default_config(network=Network.MAINNET) # Enable stable balance with USDB conversion config.stable_balance_config = StableBalanceConfig( tokens=[StableBalanceToken( label="USDB", token_identifier="btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87", )], default_active_label="USDB", ) ``` ### Go ```go config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) // Enable stable balance with USDB conversion defaultActiveLabel := "USDB" stableBalanceConfig := breez_sdk_spark.StableBalanceConfig{ Tokens: []breez_sdk_spark.StableBalanceToken{ { Label: "USDB", TokenIdentifier: "btkn1xgrvjwey5ngcagvap2dzzvsy4uk8ua9x69k82dwvt5e7ef9drm9qztux87", }, }, DefaultActiveLabel: &defaultActiveLabel, } config.StableBalanceConfig = &stableBalanceConfig ``` ## SOCKS5 proxy API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.ProxyConfig.html Routes the connections the SDK opens through a SOCKS5 proxy, such as a local Tor daemon. Hostnames are resolved by the proxy rather than locally, and a connection that cannot be established through it fails rather than falling back to a direct one. Unset (the default) connects directly. Not supported on WASM. See the [SOCKS5 proxy](/guide/proxy.md) page for what is covered and which combinations are rejected. ### Rust ```rust let mut config = default_config(Network::Mainnet); config.api_key = Some("".to_string()); // Route the SDK's connections through a SOCKS5 proxy, such as a local Tor // daemon. Set username and password together for proxies that require // RFC 1929 authentication. config.proxy = Some(ProxyConfig { host: "127.0.0.1".to_string(), port: 9050, username: None, password: None, }); ``` ### Swift ```swift var config = defaultConfig(network: Network.mainnet) config.apiKey = "" // Route the SDK's connections through a SOCKS5 proxy, such as a local Tor // daemon. Set username and password together for proxies that require // RFC 1929 authentication. config.proxy = ProxyConfig( host: "127.0.0.1", port: 9050, username: nil, password: nil ) ``` ### Kotlin ```kotlin val config = defaultConfig(Network.MAINNET) config.apiKey = "" // Route the SDK's connections through a SOCKS5 proxy, such as a local Tor // daemon. Set username and password together for proxies that require // RFC 1929 authentication. config.proxy = ProxyConfig( host = "127.0.0.1", port = 9050u, username = null, password = null, ) ``` ### C# ```csharp // Route the SDK's connections through a SOCKS5 proxy, such as a local // Tor daemon. Set username and password together for proxies that // require RFC 1929 authentication. var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { apiKey = "", proxy = new ProxyConfig( host: "127.0.0.1", port: 9050, username: null, password: null) }; ``` ### React Native ```typescript const config = defaultConfig(Network.Mainnet) config.apiKey = '' // Route the SDK's connections through a SOCKS5 proxy, such as a local Tor // daemon. Set username and password together for proxies that require // RFC 1929 authentication. config.proxy = { host: '127.0.0.1', port: 9050, username: undefined, password: undefined } ``` ### Flutter ```dart // Route the SDK's connections through a SOCKS5 proxy, such as a local Tor // daemon. Set username and password together for proxies that require // RFC 1929 authentication. final config = defaultConfig(network: Network.mainnet).copyWith( apiKey: "", proxy: const ProxyConfig( host: "127.0.0.1", port: 9050, username: null, password: null, ), ); ``` ### Python ```python config = default_config(network=Network.MAINNET) config.api_key = "" # Route the SDK's connections through a SOCKS5 proxy, such as a local Tor # daemon. Set username and password together for proxies that require # RFC 1929 authentication. config.proxy = ProxyConfig( host="127.0.0.1", port=9050, username=None, password=None, ) ``` ### Go ```go config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) apiKey := "" config.ApiKey = &apiKey // Route the SDK's connections through a SOCKS5 proxy, such as a local Tor // daemon. Set username and password together for proxies that require // RFC 1929 authentication. config.Proxy = &breez_sdk_spark.ProxyConfig{ Host: "127.0.0.1", Port: 9050, Username: nil, Password: nil, } ``` ## USDC/USDT API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.CrossChainConfig.html USDC/USDT (send and receive) requires explicit opt-in: `default_config` leaves `cross_chain_config` unset. Set it to a default `CrossChainConfig` to enable the feature, or to your own to override the slippage default. The SDK only returns routes whose destination is USDC or USDT on a supported chain. Constraints: - **Mainnet only**: `validate` rejects a set `cross_chain_config` on any network other than mainnet. - **Background tasks required**: both providers run background monitors that reconcile delivery status onto the local payment row, so `cross_chain_config` is incompatible with `background_tasks_enabled` disabled. `default_server_config` leaves the field unset for this reason. ### Rust ```rust let mut config = default_config(Network::Mainnet); config.api_key = Some("".to_string()); // Set to enable cross-chain payments. Slippage override is optional (10 to 500 bps). config.cross_chain_config = Some(CrossChainConfig { default_slippage_bps: Some(50), default_target_overpay_bps: None, }); ``` ### Swift ```swift var config = defaultConfig(network: Network.mainnet) config.apiKey = "" // Set to enable cross-chain payments. Slippage override is optional (10 to 500 bps). config.crossChainConfig = CrossChainConfig( defaultSlippageBps: 50, defaultTargetOverpayBps: nil ) ``` ### Kotlin ```kotlin val config = defaultConfig(Network.MAINNET) config.apiKey = "" // Set to enable cross-chain payments. Slippage override is optional (10 to 500 bps). config.crossChainConfig = CrossChainConfig( defaultSlippageBps = 50u, defaultTargetOverpayBps = null, ) ``` ### C# ```csharp // Set to enable cross-chain payments. Slippage override is optional (10 to 500 bps). var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { apiKey = "", crossChainConfig = new CrossChainConfig( defaultSlippageBps: 50, defaultTargetOverpayBps: null) }; ``` ### Javascript (Wasm) ```typescript const config = defaultConfig('mainnet') config.apiKey = '' // Set to enable cross-chain payments. Slippage override is optional (10 to 500 bps). config.crossChainConfig = { defaultSlippageBps: 50 } ``` ### React Native ```typescript const config = defaultConfig(Network.Mainnet) config.apiKey = '' // Set to enable cross-chain payments. Slippage override is optional (10 to 500 bps). config.crossChainConfig = { defaultSlippageBps: 50, defaultTargetOverpayBps: undefined } ``` ### Flutter ```dart // Set to enable cross-chain payments. Slippage override is optional (10 to 500 bps). final config = defaultConfig(network: Network.mainnet).copyWith( apiKey: "", crossChainConfig: const CrossChainConfig( defaultSlippageBps: 50, defaultTargetOverpayBps: null, ), ); ``` ### Python ```python config = default_config(network=Network.MAINNET) config.api_key = "" # Set to enable cross-chain payments. Slippage override is optional (10 to 500 bps). config.cross_chain_config = CrossChainConfig( default_slippage_bps=50, default_target_overpay_bps=None, ) ``` ### Go ```go config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) apiKey := "" config.ApiKey = &apiKey // Set to enable cross-chain payments. Slippage override is optional (10 to 500 bps). defaultSlippageBps := uint32(50) config.CrossChainConfig = &breez_sdk_spark.CrossChainConfig{ DefaultSlippageBps: &defaultSlippageBps, DefaultTargetOverpayBps: nil, } ``` The `default_slippage_bps` field sets the per-instance slippage default applied when the per-request `max_slippage_bps` is unset. It must be in the 10 to 500 basis-point range; when `default_slippage_bps` itself is unset, the SDK falls back to a built-in default of 100 bps (1%). See [USDC/USDT](/guide/cross_chain.md) for the provider lineup, status lifecycle, retry-safety semantics, and limitations. --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [SOCKS5 proxy](/guide/proxy.md)** # Routing traffic through a SOCKS5 proxy Set `proxy` on the [config](/guide/config.md) to send the connections the SDK opens through a SOCKS5 proxy, such as a local Tor daemon. ## Rust ```rust let mut config = default_config(Network::Mainnet); config.api_key = Some("".to_string()); // Route the SDK's connections through a SOCKS5 proxy, such as a local Tor // daemon. Set username and password together for proxies that require // RFC 1929 authentication. config.proxy = Some(ProxyConfig { host: "127.0.0.1".to_string(), port: 9050, username: None, password: None, }); ``` ## Swift ```swift var config = defaultConfig(network: Network.mainnet) config.apiKey = "" // Route the SDK's connections through a SOCKS5 proxy, such as a local Tor // daemon. Set username and password together for proxies that require // RFC 1929 authentication. config.proxy = ProxyConfig( host: "127.0.0.1", port: 9050, username: nil, password: nil ) ``` ## Kotlin ```kotlin val config = defaultConfig(Network.MAINNET) config.apiKey = "" // Route the SDK's connections through a SOCKS5 proxy, such as a local Tor // daemon. Set username and password together for proxies that require // RFC 1929 authentication. config.proxy = ProxyConfig( host = "127.0.0.1", port = 9050u, username = null, password = null, ) ``` ## C# ```csharp // Route the SDK's connections through a SOCKS5 proxy, such as a local // Tor daemon. Set username and password together for proxies that // require RFC 1929 authentication. var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { apiKey = "", proxy = new ProxyConfig( host: "127.0.0.1", port: 9050, username: null, password: null) }; ``` ## React Native ```typescript const config = defaultConfig(Network.Mainnet) config.apiKey = '' // Route the SDK's connections through a SOCKS5 proxy, such as a local Tor // daemon. Set username and password together for proxies that require // RFC 1929 authentication. config.proxy = { host: '127.0.0.1', port: 9050, username: undefined, password: undefined } ``` ## Flutter ```dart // Route the SDK's connections through a SOCKS5 proxy, such as a local Tor // daemon. Set username and password together for proxies that require // RFC 1929 authentication. final config = defaultConfig(network: Network.mainnet).copyWith( apiKey: "", proxy: const ProxyConfig( host: "127.0.0.1", port: 9050, username: null, password: null, ), ); ``` ## Python ```python config = default_config(network=Network.MAINNET) config.api_key = "" # Route the SDK's connections through a SOCKS5 proxy, such as a local Tor # daemon. Set username and password together for proxies that require # RFC 1929 authentication. config.proxy = ProxyConfig( host="127.0.0.1", port=9050, username=None, password=None, ) ``` ## Go ```go config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) apiKey := "" config.ApiKey = &apiKey // Route the SDK's connections through a SOCKS5 proxy, such as a local Tor // daemon. Set username and password together for proxies that require // RFC 1929 authentication. config.Proxy = &breez_sdk_spark.ProxyConfig{ Host: "127.0.0.1", Port: 9050, Username: nil, Password: nil, } ``` Set `username` and `password` together for proxies that require RFC 1929 authentication. Setting only one of them is rejected. ## What the proxy covers Every connection the SDK opens, HTTP and gRPC alike. Hostnames are resolved **by the proxy**, never locally, so no DNS query reveals which host you are reaching. BIP353 name lookups switch from plain DNS to DNS-over-HTTPS for the same reason: plain DNS is UDP, which a SOCKS5 proxy does not carry, so those queries would otherwise escape the tunnel. They stay DNSSEC-verified either way. Only the proxy's own address is resolved locally, which is unavoidable: it is the one host that cannot be reached through itself. ## Failing closed A connection that cannot be established through the proxy fails. The SDK never retries it directly, and setting a proxy also disables system-proxy autodetection, so no environment variable can route traffic around it. A configuration the SDK cannot honour is rejected where it is supplied, rather than partly applied: at `connect`, or at the constructor of a component built outside the SDK. | Rejected combination | Why | |---|---| | A proxy on WASM | The browser owns connection setup and exposes no proxy control. | | `proxy` with `connections_per_operator` above 1 | Balanced operator connections build their own connectors and cannot be routed. | | A proxy carrying credentials on `PasskeyConfig` | Nostr relay connections cannot authenticate to a proxy. Wallet labels live on those relays and are one of the salts a wallet seed derives from, so a label that cannot be published cannot be recovered: this fails before a wallet exists rather than after one is funded. | ## Shared SDK Context A [shared SDK Context](/guide/customizing.md#with-shared-context) owns the pooled HTTP client and gRPC channels, so the proxy has to be set on `SdkContextConfig` as well. It must match the `proxy` on the `Config` of every SDK built from that context: the SDK rejects a mismatch at `connect`, since a disagreement would mean part of the traffic bypassed the proxy. ## Components built outside the SDK A few APIs run without an SDK instance, so they cannot pick the setting up on their own. Pass the same proxy to each one you use: - `get_spark_status`, via `GetSparkStatusRequest` - `new_rest_chain_service`, via `NewRestChainServiceRequest` - The Turnkey signer, via `TurnkeyConfig` - The passkey client, via `PasskeyConfig`. A proxy carrying credentials is rejected when the client is constructed (see above). A service you supply yourself through `with_chain_service`, `with_fiat_service`, `with_lnurl_client` or `with_lnurl_server_client` already owns its transport, which the SDK cannot inspect or re-route. Make it connect through the proxy yourself; the SDK logs a warning when it sees one alongside a proxy. `with_rest_chain_service` is built on the SDK's own client and is proxied automatically. ## JavaScript and WASM A SOCKS5 proxy cannot be honoured in a browser, and setting `proxy` on a WASM build is an error rather than a silent direct connection. In Node, route the SDK by installing a proxy dispatcher on the global `fetch` before connecting, which covers both the HTTP and gRPC calls the WASM build makes: ```javascript import { setGlobalDispatcher, ProxyAgent } from 'undici' setGlobalDispatcher(new ProxyAgent('socks5://127.0.0.1:9050')) ``` --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Custom leaf optimization](/guide/optimize.md)** # Custom leaf optimization The SDK implements a configurable Spark leaf optimization process. It supports two optimization policies: - **Maximize unilateral exit efficiency**: aims to minimize the number of leaves, reducing costs for unilaterally exiting Bitcoin funds. - **Increase payment speed**: maintains multiple copies of each leaf denomination to reduce the need for swaps during Bitcoin payments. ## Configuring the optimization policy The optimization behavior is controlled by the **multiplicity** setting. Setting it to 0 fully optimizes for unilateral exit efficiency, while values greater than 0 also optimize for payment speed. Higher values prioritize payment speed more aggressively, resulting in higher unilateral exit costs but faster payments, especially for bursts of transactions. For most end-user wallets, a multiplicity of 1-5 is recommended. Values above 5 are intended for high-throughput server environments that require maximum transactions per second (TPS) and should not be used in end-user wallet applications due to the significantly higher unilateral exit costs. See [Configuration](/guide/config.md#optimization-configuration) to learn how to set the multiplicity. ### Impact on payment speed Multiplicity defines how many copies of each leaf denomination the SDK maintains. A higher multiplicity provides more flexibility in leaf combinations, reducing the frequency of swaps during payments. However, the exact number of swap-free payments depends on transaction amounts and patterns. With automatic optimization, which is enabled by default, a multiplicity of 1 (the default) works well for most single-user applications with low payment frequency, eliminating the need for swaps in the vast majority of payment scenarios. Higher multiplicities are better suited for high-volume payment processing. ### Impact on unilateral exit costs Maintaining more leaves increases the total cost of unilaterally exiting funds, as each leaf incurs its own exit fee regardless of the leaf's value. This makes small denomination leaves cost-ineffective to exit. **Developer note** Keep multiplicity as low as possible while meeting your performance requirements. A high multiplicity can make unilateral exits prohibitively expensive. ## Controlling optimization timing The optimization process reorganizes leaves by swapping them to achieve optimal denominations. During this process, funds in leaves being swapped become temporarily unavailable for payments, which can delay transaction processing. By default, the SDK automatically triggers optimization after each payment (sent or received). For applications requiring more control, you can disable automatic optimization in the [configuration](/guide/config.md#optimization-configuration) and drive it manually using `optimize_leaves`. ### Run optimization to completion API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.optimize_leaves Call `optimize_leaves` with an `OptimizeLeavesRequest` using the default `OptimizationMode::Full` mode to run optimization until no further work is productive. The call blocks for the duration of the run and returns an `OptimizeLeavesResponse` whose `outcome` is `OptimizationOutcome::Completed` with the number of rounds executed. A `rounds_executed` of `0` means the wallet was already optimal at call time. #### Rust ```rust let outcome = sdk .optimize_leaves(OptimizeLeavesRequest::default()) .await? .outcome; match outcome { OptimizationOutcome::Completed { rounds_executed } => { if rounds_executed == 0 { info!("Optimization skipped — wallet already optimal"); } else { info!("Optimization completed in {} rounds", rounds_executed); } } OptimizationOutcome::InProgress => { // Full mode runs to completion in one call, so InProgress is // not reachable here. unreachable!("Full mode never returns InProgress"); } } ``` #### Swift ```swift let outcome = try await sdk.optimizeLeaves(request: OptimizeLeavesRequest(mode: .full)).outcome switch outcome { case .completed(let roundsExecuted): if roundsExecuted == 0 { print("Optimization skipped — wallet already optimal") } else { print("Optimization completed in \(roundsExecuted) rounds") } case .inProgress: // Full mode runs to completion in one call, so InProgress is // not reachable here. fatalError("Full mode never returns InProgress") } ``` #### Kotlin ```kotlin val outcome = sdk.optimizeLeaves(OptimizeLeavesRequest(mode = OptimizationMode.FULL)).outcome when (outcome) { is OptimizationOutcome.Completed -> { if (outcome.roundsExecuted == 0u) { // Log.v("Breez", "Optimization skipped — wallet already optimal") } else { // Log.v("Breez", "Optimization completed in ${outcome.roundsExecuted} rounds") } } is OptimizationOutcome.InProgress -> { // Full mode runs to completion in one call, so InProgress is // not reachable here. throw IllegalStateException("Full mode never returns InProgress") } } ``` #### C# ```csharp var outcome = (await sdk.OptimizeLeaves(new OptimizeLeavesRequest(OptimizationMode.Full))).outcome; switch (outcome) { case OptimizationOutcome.Completed { roundsExecuted: var roundsExecuted }: if (roundsExecuted == 0) { Console.WriteLine("Optimization skipped — wallet already optimal"); } else { Console.WriteLine($"Optimization completed in {roundsExecuted} rounds"); } break; case OptimizationOutcome.InProgress: // Full mode runs to completion in one call, so InProgress is // not reachable here. throw new InvalidOperationException("Full mode never returns InProgress"); } ``` #### Javascript (Wasm) ```typescript const outcome = (await sdk.optimizeLeaves({ mode: 'full' })).outcome switch (outcome.type) { case 'completed': { if (outcome.roundsExecuted === 0) { console.log('Optimization skipped — wallet already optimal') } else { console.log(`Optimization completed in ${outcome.roundsExecuted} rounds`) } break } case 'inProgress': { // Full mode runs to completion in one call, so inProgress is // not reachable here. break } } ``` #### React Native ```typescript const outcome = (await sdk.optimizeLeaves({ mode: OptimizationMode.Full })).outcome if (outcome.tag === OptimizationOutcome_Tags.Completed) { if (outcome.inner.roundsExecuted === 0) { console.log('Optimization skipped — wallet already optimal') } else { console.log(`Optimization completed in ${outcome.inner.roundsExecuted} rounds`) } } else if (outcome.tag === OptimizationOutcome_Tags.InProgress) { // Full mode runs to completion in one call, so InProgress is // not reachable here. } ``` #### Flutter ```dart final outcome = (await sdk.optimizeLeaves( request: OptimizeLeavesRequest(mode: OptimizationMode.full))) .outcome; switch (outcome) { case OptimizationOutcome_Completed(:final roundsExecuted): if (roundsExecuted == 0) { print("Optimization skipped — wallet already optimal"); } else { print("Optimization completed in $roundsExecuted rounds"); } break; case OptimizationOutcome_InProgress(): // Full mode runs to completion in one call, so InProgress is // not reachable here. throw StateError("Full mode never returns InProgress"); } ``` #### Python ```python response = await sdk.optimize_leaves(OptimizeLeavesRequest(mode=OptimizationMode.FULL)) outcome = response.outcome if isinstance(outcome, OptimizationOutcome.COMPLETED): if outcome.rounds_executed == 0: logging.debug("Optimization skipped — wallet already optimal") else: logging.debug(f"Optimization completed in {outcome.rounds_executed} rounds") elif isinstance(outcome, OptimizationOutcome.IN_PROGRESS): raise AssertionError("Full mode never returns IN_PROGRESS") ``` #### Go ```go response, err := sdk.OptimizeLeaves(breez_sdk_spark.OptimizeLeavesRequest{ Mode: breez_sdk_spark.OptimizationModeFull, }) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed } return err } switch o := response.Outcome.(type) { case breez_sdk_spark.OptimizationOutcomeCompleted: if o.RoundsExecuted == 0 { log.Printf("Optimization skipped — wallet already optimal") } else { log.Printf("Optimization completed in %v rounds", o.RoundsExecuted) } case breez_sdk_spark.OptimizationOutcomeInProgress: // Full mode runs to completion in one call, so InProgress is // not reachable here. log.Panicf("Full mode never returns InProgress") } ``` ### Run optimization one round at a time API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.optimize_leaves To display progress or cancel between rounds, pass an `OptimizeLeavesRequest` with `OptimizationMode::SingleRound`. Each call executes one round and the response `outcome` is `OptimizationOutcome::InProgress` (more work remains) or `OptimizationOutcome::Completed` (terminal — either the planner confirmed this swap finished optimization, or a `rounds_executed` of `0` indicates the wallet was already optimal). Cancel between rounds simply by stopping the loop. #### Rust ```rust let mut rounds_executed = 0u32; loop { let request = OptimizeLeavesRequest { mode: OptimizationMode::SingleRound, }; match sdk.optimize_leaves(request).await?.outcome { OptimizationOutcome::InProgress => { rounds_executed += 1; info!("Executed round {}", rounds_executed); } OptimizationOutcome::Completed { rounds_executed: this_round, } => { rounds_executed += this_round; if rounds_executed == 0 { info!("Optimization skipped — wallet already optimal"); } else { info!("Optimization done after {} rounds", rounds_executed); } break; } } } ``` #### Swift ```swift var roundsExecuted: UInt32 = 0 loop: while true { let outcome = try await sdk.optimizeLeaves( request: OptimizeLeavesRequest(mode: .singleRound) ).outcome switch outcome { case .inProgress: roundsExecuted += 1 print("Executed round \(roundsExecuted)") case .completed(let thisRound): roundsExecuted += thisRound if roundsExecuted == 0 { print("Optimization skipped — wallet already optimal") } else { print("Optimization done after \(roundsExecuted) rounds") } break loop } } ``` #### Kotlin ```kotlin var roundsExecuted: UInt = 0u while (true) { val outcome = sdk.optimizeLeaves( OptimizeLeavesRequest(mode = OptimizationMode.SINGLE_ROUND) ).outcome when (outcome) { is OptimizationOutcome.InProgress -> { roundsExecuted += 1u // Log.v("Breez", "Executed round $roundsExecuted") } is OptimizationOutcome.Completed -> { roundsExecuted += outcome.roundsExecuted if (roundsExecuted == 0u) { // Log.v("Breez", "Optimization skipped — wallet already optimal") } else { // Log.v("Breez", "Optimization done after $roundsExecuted rounds") } break } } } ``` #### C# ```csharp uint roundsExecuted = 0; while (true) { var outcome = (await sdk.OptimizeLeaves( new OptimizeLeavesRequest(OptimizationMode.SingleRound) )).outcome; if (outcome is OptimizationOutcome.InProgress) { roundsExecuted += 1; Console.WriteLine($"Executed round {roundsExecuted}"); } else if (outcome is OptimizationOutcome.Completed { roundsExecuted: var n }) { roundsExecuted += n; if (roundsExecuted == 0) { Console.WriteLine("Optimization skipped — wallet already optimal"); } else { Console.WriteLine($"Optimization done after {roundsExecuted} rounds"); } break; } } ``` #### Javascript (Wasm) ```typescript let roundsExecuted = 0 while (true) { const outcome = (await sdk.optimizeLeaves({ mode: 'singleRound' })).outcome if (outcome.type === 'inProgress') { roundsExecuted += 1 console.log(`Executed round ${roundsExecuted}`) } else if (outcome.type === 'completed') { roundsExecuted += outcome.roundsExecuted if (roundsExecuted === 0) { console.log('Optimization skipped — wallet already optimal') } else { console.log(`Optimization done after ${roundsExecuted} rounds`) } break } } ``` #### React Native ```typescript let roundsExecuted = 0 while (true) { const outcome: OptimizationOutcome = ( await sdk.optimizeLeaves({ mode: OptimizationMode.SingleRound }) ).outcome if (outcome.tag === OptimizationOutcome_Tags.InProgress) { roundsExecuted += 1 console.log(`Executed round ${roundsExecuted}`) } else if (outcome.tag === OptimizationOutcome_Tags.Completed) { roundsExecuted += outcome.inner.roundsExecuted if (roundsExecuted === 0) { console.log('Optimization skipped — wallet already optimal') } else { console.log(`Optimization done after ${roundsExecuted} rounds`) } break } } ``` #### Flutter ```dart var roundsExecuted = 0; while (true) { final outcome = (await sdk.optimizeLeaves( request: OptimizeLeavesRequest(mode: OptimizationMode.singleRound))).outcome; switch (outcome) { case OptimizationOutcome_InProgress(): roundsExecuted += 1; print("Executed round $roundsExecuted"); break; case OptimizationOutcome_Completed(roundsExecuted: var n): roundsExecuted += n; if (roundsExecuted == 0) { print("Optimization skipped — wallet already optimal"); } else { print("Optimization done after $roundsExecuted rounds"); } return; } } ``` #### Python ```python rounds_executed = 0 while True: response = await sdk.optimize_leaves( OptimizeLeavesRequest(mode=OptimizationMode.SINGLE_ROUND) ) outcome = response.outcome if isinstance(outcome, OptimizationOutcome.IN_PROGRESS): rounds_executed += 1 logging.debug(f"Executed round {rounds_executed}") elif isinstance(outcome, OptimizationOutcome.COMPLETED): rounds_executed += outcome.rounds_executed if rounds_executed == 0: logging.debug("Optimization skipped — wallet already optimal") else: logging.debug(f"Optimization done after {rounds_executed} rounds") break ``` #### Go ```go var roundsExecuted uint32 = 0 for { response, err := sdk.OptimizeLeaves(breez_sdk_spark.OptimizeLeavesRequest{ Mode: breez_sdk_spark.OptimizationModeSingleRound, }) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed } return err } switch o := response.Outcome.(type) { case breez_sdk_spark.OptimizationOutcomeInProgress: roundsExecuted += 1 log.Printf("Executed round %v", roundsExecuted) case breez_sdk_spark.OptimizationOutcomeCompleted: roundsExecuted += o.RoundsExecuted if roundsExecuted == 0 { log.Printf("Optimization skipped — wallet already optimal") } else { log.Printf("Optimization done after %v rounds", roundsExecuted) } return nil } } ``` **Developer note** If `optimize_leaves` is invoked while another optimization run (auto or manual) is already in flight, it returns `SdkError::OptimizationAlreadyRunning`. The SDK may also preempt a manual run to free leaves for a higher-priority payment, in which case the call returns `SdkError::OptimizationCancelled`. ## Auto-optimization events When automatic optimization is enabled, the SDK emits `SdkEvent::AutoOptimization` events so your application can track the background optimizer's progress. Manual `optimize_leaves` calls do not emit these events — inspect their return value instead. See [Listening to events](/guide/events.md) for subscription instructions. ### Rust ```rust match event { AutoOptimizationEvent::Started { total_rounds } => { info!("Auto-optimization started with {} rounds", total_rounds); } AutoOptimizationEvent::RoundCompleted { current_round, total_rounds, } => { info!( "Auto-optimization round {} of {} completed", current_round, total_rounds ); } AutoOptimizationEvent::Completed => { info!("Auto-optimization completed successfully"); } AutoOptimizationEvent::Cancelled => { info!("Auto-optimization was cancelled"); } AutoOptimizationEvent::Failed { error } => { info!("Auto-optimization failed: {}", error); } AutoOptimizationEvent::Skipped => { info!("Auto-optimization was skipped because leaves are already optimal"); } } ``` ### Swift ```swift switch event { case .started(let totalRounds): print("Auto-optimization started with \(totalRounds) rounds") case .roundCompleted(let currentRound, let totalRounds): print("Auto-optimization round \(currentRound) of \(totalRounds) completed") case .completed: print("Auto-optimization completed successfully") case .cancelled: print("Auto-optimization was cancelled") case .failed(let error): print("Auto-optimization failed: \(error)") case .skipped: print("Auto-optimization was skipped because leaves are already optimal") } ``` ### Kotlin ```kotlin when (optimizationEvent) { is AutoOptimizationEvent.Started -> { // Log.v("Breez", "Auto-optimization started with ${optimizationEvent.totalRounds} rounds") } is AutoOptimizationEvent.RoundCompleted -> { // Log.v("Breez", "Auto-optimization round // ${optimizationEvent.currentRound} of // ${optimizationEvent.totalRounds} completed") } is AutoOptimizationEvent.Completed -> { // Log.v("Breez", "Auto-optimization completed successfully") } is AutoOptimizationEvent.Cancelled -> { // Log.v("Breez", "Auto-optimization was cancelled") } is AutoOptimizationEvent.Failed -> { // Log.v("Breez", "Auto-optimization failed: ${optimizationEvent.error}") } is AutoOptimizationEvent.Skipped -> { // Log.v("Breez", "Auto-optimization was skipped because leaves are already optimal") } } ``` ### C# ```csharp switch (optimizationEvent) { case AutoOptimizationEvent.Started { totalRounds: var totalRounds }: Console.WriteLine($"Auto-optimization started with {totalRounds} rounds"); break; case AutoOptimizationEvent.RoundCompleted { currentRound: var currentRound, totalRounds: var totalRounds }: Console.WriteLine($"Auto-optimization round {currentRound} of {totalRounds} completed"); break; case AutoOptimizationEvent.Completed: Console.WriteLine("Auto-optimization completed successfully"); break; case AutoOptimizationEvent.Cancelled: Console.WriteLine("Auto-optimization was cancelled"); break; case AutoOptimizationEvent.Failed { error: var error }: Console.WriteLine($"Auto-optimization failed: {error}"); break; case AutoOptimizationEvent.Skipped: Console.WriteLine("Auto-optimization was skipped because leaves are already optimal"); break; } ``` ### Javascript (Wasm) ```typescript switch (event.type) { case 'started': { console.log(`Auto-optimization started with ${event.totalRounds} rounds`) break } case 'roundCompleted': { console.log(`Auto-optimization round ${event.currentRound} of ${event.totalRounds} completed`) break } case 'completed': { console.log('Auto-optimization completed successfully') break } case 'cancelled': { console.log('Auto-optimization was cancelled') break } case 'failed': { console.log(`Auto-optimization failed: ${event.error}`) break } case 'skipped': { console.log('Auto-optimization was skipped because leaves are already optimal') break } } ``` ### React Native ```typescript if (optimizationEvent.tag === AutoOptimizationEvent_Tags.Started) { console.log(`Auto-optimization started with ${optimizationEvent.inner.totalRounds} rounds`) } else if (optimizationEvent.tag === AutoOptimizationEvent_Tags.RoundCompleted) { console.log( `Auto-optimization round ${optimizationEvent.inner.currentRound} of ` + `${optimizationEvent.inner.totalRounds} completed` ) } else if (optimizationEvent.tag === AutoOptimizationEvent_Tags.Completed) { console.log('Auto-optimization completed successfully') } else if (optimizationEvent.tag === AutoOptimizationEvent_Tags.Cancelled) { console.log('Auto-optimization was cancelled') } else if (optimizationEvent.tag === AutoOptimizationEvent_Tags.Failed) { console.log(`Auto-optimization failed: ${optimizationEvent.inner.error}`) } else if (optimizationEvent.tag === AutoOptimizationEvent_Tags.Skipped) { console.log('Auto-optimization was skipped because leaves are already optimal') } ``` ### Flutter ```dart switch (optimizationEvent) { case AutoOptimizationEvent_Started(totalRounds: var totalRounds): print("Auto-optimization started with $totalRounds rounds"); break; case AutoOptimizationEvent_RoundCompleted( currentRound: var currentRound, totalRounds: var totalRounds ): print("Auto-optimization round $currentRound of $totalRounds completed"); break; case AutoOptimizationEvent_Completed(): print("Auto-optimization completed successfully"); break; case AutoOptimizationEvent_Cancelled(): print("Auto-optimization was cancelled"); break; case AutoOptimizationEvent_Failed(error: var error): print("Auto-optimization failed: $error"); break; case AutoOptimizationEvent_Skipped(): print("Auto-optimization was skipped because leaves are already optimal"); break; } ``` ### Python ```python if isinstance(event, AutoOptimizationEvent.STARTED): logging.debug(f"Auto-optimization started with {event.total_rounds} rounds") elif isinstance(event, AutoOptimizationEvent.ROUND_COMPLETED): logging.debug(f"Auto-optimization round {event.current_round} of " f"{event.total_rounds} completed") elif isinstance(event, AutoOptimizationEvent.COMPLETED): logging.debug("Auto-optimization completed successfully") elif isinstance(event, AutoOptimizationEvent.CANCELLED): logging.debug("Auto-optimization was cancelled") elif isinstance(event, AutoOptimizationEvent.FAILED): logging.debug(f"Auto-optimization failed: {event.error}") elif isinstance(event, AutoOptimizationEvent.SKIPPED): logging.debug("Auto-optimization was skipped because leaves are already optimal") ``` ### Go ```go switch event := optimizationEvent.(type) { case breez_sdk_spark.AutoOptimizationEventStarted: log.Printf("Auto-optimization started with %v rounds", event.TotalRounds) case breez_sdk_spark.AutoOptimizationEventRoundCompleted: log.Printf("Auto-optimization round %v of %v completed", event.CurrentRound, event.TotalRounds) case breez_sdk_spark.AutoOptimizationEventCompleted: log.Printf("Auto-optimization completed successfully") case breez_sdk_spark.AutoOptimizationEventCancelled: log.Printf("Auto-optimization was cancelled") case breez_sdk_spark.AutoOptimizationEventFailed: log.Printf("Auto-optimization failed: %v", event.Error) case breez_sdk_spark.AutoOptimizationEventSkipped: log.Printf("Auto-optimization was skipped because leaves are already optimal") } ``` --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Conditional Payments](/guide/htlcs.md)** # Conditional Payments Conditional payments use Hash Time-Locked Contracts (HTLCs) to lock funds with a cryptographic hash of a secret preimage and an expiration time. The payment can only be claimed by revealing the preimage before expiration. If not claimed in time, the funds are automatically returned to the sender. This enables use cases like atomic cross-chain swaps. The SDK supports both sending conditional payments via Spark HTLCs and receiving them via HODL invoices. **Developer note** Preimages are required to be unique and are not managed by the SDK. It is your responsibility as a developer to manage them, including how to generate them, store them, and provide them when claiming payments. ## Sending Spark HTLC payments HTLC payments use the standard payment API described in [Sending payments](/guide/send_payment.md). To create an HTLC payment, prepare the payment normally, then provide the Spark HTLC options when [sending](/guide/send_payment.md#spark). These options include the payment hash (SHA-256 hash of the preimage) and the expiry duration. ### Rust ```rust let payment_request = "".to_string(); // Set the amount you wish to pay the receiver let amount_sats = Some(50_000); let prepare_request = PrepareSendPaymentRequest { payment_request: PaymentRequest::Input { input: payment_request, }, amount: amount_sats, token_identifier: None, conversion_options: None, fee_policy: None, }; let prepare_response = sdk.prepare_send_payment(prepare_request).await?; // If the fees are acceptable, continue to create the HTLC Payment if let SendPaymentMethod::SparkAddress { fee, .. } = prepare_response.payment_method { info!("Fees: {} sats", fee); } let preimage = "<32-byte unique preimage hex>"; let preimage_bytes = hex::decode(preimage)?; let payment_hash_bytes = sha256::digest(preimage_bytes); let payment_hash = hex::encode(payment_hash_bytes); // Set the HTLC options let options = SendPaymentOptions::SparkAddress { htlc_options: Some(SparkHtlcOptions { payment_hash, expiry_duration_secs: 1000, }), }; let request = SendPaymentRequest { prepare_response, options: Some(options), idempotency_key: None, }; let send_response = sdk.send_payment(request).await?; let payment = send_response.payment; ``` ### Swift ```swift let paymentRequest = "" // Set the amount you wish to pay the receiver let amountSats = BInt(50_000) let prepareRequest = PrepareSendPaymentRequest( paymentRequest: .input(input: paymentRequest), amount: amountSats, tokenIdentifier: nil, conversionOptions: nil, feePolicy: nil ) let prepareResponse = try await sdk.prepareSendPayment(request: prepareRequest) // If the fees are acceptable, continue to create the HTLC Payment if case let .sparkAddress(_, fee, _) = prepareResponse.paymentMethod { print("Fees: \(fee) sats") } let preimage = "<32-byte unique preimage hex>" let preimageData = Data(hexString: preimage)! let paymentHashDigest = SHA256.hash(data: preimageData) let paymentHash = Data(paymentHashDigest).hexEncodedString() // Set the HTLC options let htlcOptions = SparkHtlcOptions( paymentHash: paymentHash, expiryDurationSecs: 1000 ) let options = SendPaymentOptions.sparkAddress(htlcOptions: htlcOptions) let request = SendPaymentRequest( prepareResponse: prepareResponse, options: options ) let sendResponse = try await sdk.sendPayment(request: request) let payment = sendResponse.payment ``` ### Kotlin ```kotlin val paymentRequest = "" // Set the amount you wish the pay the receiver // Kotlin MPP (BigInteger from com.ionspin.kotlin.bignum.integer) val amountSats = BigInteger.fromLong(50_000L) // Android (BigInteger from java.math) // val amountSats = BigInteger.valueOf(50_000L) try { val prepareRequest = PrepareSendPaymentRequest( paymentRequest = PaymentRequest.Input(input = paymentRequest), amount = amountSats, tokenIdentifier = null, conversionOptions = null, feePolicy = null, ) val prepareResponse = sdk.prepareSendPayment(prepareRequest) // If the fees are acceptable, continue to create the HTLC Payment val paymentMethod = prepareResponse.paymentMethod if (paymentMethod is SendPaymentMethod.SparkAddress) { val fee = paymentMethod.fee // Log.v("Breez", "Fees: ${fee} sats") } val preimage = "<32-byte unique preimage hex>" val preimageBytes = preimage.hexToByteArray() val digest = SHA256() digest.update(preimageBytes) val paymentHashBytes = digest.digest() val paymentHash = paymentHashBytes.toHexString() // Set the HTLC options val htlcOptions = SparkHtlcOptions( paymentHash = paymentHash, expiryDurationSecs = 1000u ) val options = SendPaymentOptions.SparkAddress(htlcOptions = htlcOptions) val request = SendPaymentRequest( prepareResponse = prepareResponse, options = options ) val sendResponse = sdk.sendPayment(request) val payment = sendResponse.payment } catch (e: Exception) { // handle error throw e } ``` ### C# ```csharp var paymentRequest = ""; // Set the amount you wish the pay the receiver ulong? amountSats = 50_000UL; var prepareRequest = new PrepareSendPaymentRequest( paymentRequest: new PaymentRequest.Input(input: paymentRequest), amount: amountSats, tokenIdentifier: null, conversionOptions: null, feePolicy: null ); var prepareResponse = await sdk.PrepareSendPayment(request: prepareRequest); // If the fees are acceptable, continue to create the HTLC Payment if (prepareResponse.paymentMethod is SendPaymentMethod.SparkAddress sparkMethod) { var fee = sparkMethod.fee; Console.WriteLine($"Fees: {fee} sats"); } var preimage = "<32-byte unique preimage hex>"; var preimageBytes = Convert.FromHexString(preimage); var paymentHashBytes = System.Security.Cryptography.SHA256.HashData(preimageBytes); var paymentHash = Convert.ToHexString(paymentHashBytes).ToLower(); // Set the HTLC options var options = new SendPaymentOptions.SparkAddress( htlcOptions: new SparkHtlcOptions( paymentHash: paymentHash, expiryDurationSecs: 1000 ) ); var request = new SendPaymentRequest( prepareResponse: prepareResponse, options: options ); var sendResponse = await sdk.SendPayment(request: request); var payment = sendResponse.payment; ``` ### Javascript (Wasm) ```typescript const paymentRequest = '' // Set the amount you wish to pay the receiver const amountSats = BigInt(50_000) const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: { type: 'input', input: paymentRequest }, amount: amountSats, tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: undefined }) // If the fees are acceptable, continue to create the HTLC Payment if (prepareResponse.paymentMethod.type === 'sparkAddress') { const fee = prepareResponse.paymentMethod.fee console.debug(`Fees: ${fee} sats`) } const preimage = '<32-byte unique preimage hex>' const preimageBuffer = Buffer.from(preimage, 'hex') const paymentHash = createHash('sha256').update(preimageBuffer).digest('hex') const sendResponse = await sdk.sendPayment({ prepareResponse, options: { type: 'sparkAddress', htlcOptions: { paymentHash, expiryDurationSecs: 1000 } } }) const payment = sendResponse.payment ``` ### React Native ```typescript const paymentRequest = '' // Set the amount you wish to pay the receiver const amountSats = BigInt(50_000) const prepareRequest = { paymentRequest: new PaymentRequest.Input({ input: paymentRequest }), amount: amountSats, tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: undefined } const prepareResponse = await sdk.prepareSendPayment(prepareRequest) // If the fees are acceptable, continue to create the HTLC Payment if (prepareResponse.paymentMethod?.tag === SendPaymentMethod_Tags.SparkAddress) { const fee = prepareResponse.paymentMethod.inner.fee console.debug(`Fees: ${fee} sats`) } const preimage = '<32-byte unique preimage hex>' const preimageBuffer = Buffer.from(preimage, 'hex') const paymentHash = createHash('sha256').update(preimageBuffer).digest('hex') // Set the HTLC options const options = new SendPaymentOptions.SparkAddress({ htlcOptions: { paymentHash, expiryDurationSecs: BigInt(1000) } }) const request = { prepareResponse, options, idempotencyKey: undefined } const sendResponse = await sdk.sendPayment(request) const payment = sendResponse.payment ``` ### Flutter ```dart String paymentRequest = ""; // Set the amount you wish the pay the receiver BigInt? amountSats = BigInt.from(50000); final prepareRequest = PrepareSendPaymentRequest( paymentRequest: PaymentRequest.input(input: paymentRequest), amount: amountSats, tokenIdentifier: null, conversionOptions: null, feePolicy: null); final prepareResponse = await sdk.prepareSendPayment(request: prepareRequest); // If the fees are acceptable, continue to create the HTLC Payment final paymentMethod = prepareResponse.paymentMethod; if (paymentMethod is SendPaymentMethod_SparkAddress) { final fee = paymentMethod.fee; print("Fees: $fee sats"); } String preimage = "<32-byte unique preimage hex>"; List preimageBytes = hex.decode(preimage); Digest paymentHashDigest = sha256.convert(preimageBytes); String paymentHash = hex.encode(paymentHashDigest.bytes); // Set the HTLC options final htlcOptions = SparkHtlcOptions( paymentHash: paymentHash, expiryDurationSecs: BigInt.from(1000)); final options = SendPaymentOptions.sparkAddress(htlcOptions: htlcOptions); final request = SendPaymentRequest(prepareResponse: prepareResponse, options: options); final sendResponse = await sdk.sendPayment(request: request); final payment = sendResponse.payment; ``` ### Python ```python payment_request = "" amount_sats = 50_000 prepare_request = PrepareSendPaymentRequest( payment_request=PaymentRequest.INPUT(input=payment_request), amount=amount_sats, token_identifier=None, conversion_options=None, fee_policy=None, ) prepare_response = await sdk.prepare_send_payment(request=prepare_request) # If the fees are acceptable, continue to create the HTLC Payment if hasattr(prepare_response.payment_method, "fee"): fee = prepare_response.payment_method.fee logging.debug(f"Fees: {fee} sats") preimage = "<32-byte unique preimage hex>" preimage_bytes = bytes.fromhex(preimage) payment_hash_bytes = hashlib.sha256(preimage_bytes).digest() payment_hash = payment_hash_bytes.hex() # Set the HTLC options options = SendPaymentOptions.SPARK_ADDRESS( htlc_options=SparkHtlcOptions( payment_hash=payment_hash, expiry_duration_secs=1000 ) ) request = SendPaymentRequest( prepare_response=prepare_response, options=options ) send_response = await sdk.send_payment(request=request) payment = send_response.payment ``` ### Go ```go paymentRequest := "" // Set the amount you wish to pay the receiver amountSats := new(big.Int).SetInt64(50_000) prepareRequest := breez_sdk_spark.PrepareSendPaymentRequest{ PaymentRequest: breez_sdk_spark.PaymentRequestInput{Input: paymentRequest}, Amount: &amountSats, TokenIdentifier: nil, ConversionOptions: nil, FeePolicy: nil, } prepareResponse, err := sdk.PrepareSendPayment(prepareRequest) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } // If the fees are acceptable, continue to create the HTLC Payment switch paymentMethod := prepareResponse.PaymentMethod.(type) { case breez_sdk_spark.SendPaymentMethodSparkAddress: fee := paymentMethod.Fee log.Printf("Fees: %v sats", fee) } preimage := "<32-byte unique preimage hex>" preimageBytes, err := hex.DecodeString(preimage) if err != nil { return nil, err } paymentHashBytes := sha256.Sum256(preimageBytes) paymentHash := hex.EncodeToString(paymentHashBytes[:]) // Set the HTLC options htlcOptions := breez_sdk_spark.SparkHtlcOptions{ PaymentHash: paymentHash, ExpiryDurationSecs: 1000, } var options breez_sdk_spark.SendPaymentOptions = breez_sdk_spark.SendPaymentOptionsSparkAddress{ HtlcOptions: &htlcOptions, } request := breez_sdk_spark.SendPaymentRequest{ PrepareResponse: prepareResponse, Options: &options, } sendResponse, err := sdk.SendPayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } payment := sendResponse.Payment ``` ## Receiving using HODL invoices API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.receive_payment You can receive using HODL invoices — Lightning invoices where the payment is held until you claim it by revealing the preimage. To create one, provide a `payment_hash` when calling `receive_payment` with the `ReceivePaymentMethod::Bolt11Invoice` payment method. ### Rust ```rust let preimage = "<32-byte unique preimage hex>"; let preimage_bytes = hex::decode(preimage)?; let payment_hash_bytes = sha256::digest(preimage_bytes); let payment_hash = hex::encode(payment_hash_bytes); let response = sdk .receive_payment(ReceivePaymentRequest { payment_method: ReceivePaymentMethod::Bolt11Invoice { description: "HODL invoice".to_string(), amount_sats: Some(50_000), expiry_secs: None, payment_hash: Some(payment_hash), receiver_identity_public_key: None, }, }) .await?; let invoice = response.payment_request; info!("HODL invoice: {invoice}"); ``` ### Swift ```swift let preimage = "<32-byte unique preimage hex>" let preimageData = Data(hexString: preimage)! let paymentHashDigest = SHA256.hash(data: preimageData) let paymentHash = Data(paymentHashDigest).hexEncodedString() let response = try await sdk.receivePayment( request: ReceivePaymentRequest( paymentMethod: ReceivePaymentMethod.bolt11Invoice( description: "HODL invoice", amountSats: 50_000, expirySecs: nil, paymentHash: paymentHash, receiverIdentityPublicKey: nil ) ) ) let invoice = response.paymentRequest print("HODL invoice: \(invoice)") ``` ### Kotlin ```kotlin try { val preimage = "<32-byte unique preimage hex>" val preimageBytes = preimage.hexToByteArray() val digest = SHA256() digest.update(preimageBytes) val paymentHashBytes = digest.digest() val paymentHash = paymentHashBytes.toHexString() val response = sdk.receivePayment( ReceivePaymentRequest( paymentMethod = ReceivePaymentMethod.Bolt11Invoice( description = "HODL invoice", amountSats = 50_000u, expirySecs = null, paymentHash = paymentHash, receiverIdentityPublicKey = null ) ) ) val invoice = response.paymentRequest // Log.v("Breez", "HODL invoice: $invoice") } catch (e: Exception) { // handle error throw e } ``` ### C# ```csharp var preimage = "<32-byte unique preimage hex>"; var preimageBytes = Convert.FromHexString(preimage); var paymentHashBytes = System.Security.Cryptography.SHA256.HashData(preimageBytes); var paymentHash = Convert.ToHexString(paymentHashBytes).ToLower(); var response = await sdk.ReceivePayment( request: new ReceivePaymentRequest( paymentMethod: new ReceivePaymentMethod.Bolt11Invoice( description: "HODL invoice", amountSats: 50_000UL, expirySecs: null, paymentHash: paymentHash, receiverIdentityPublicKey: null ) ) ); var invoice = response.paymentRequest; Console.WriteLine($"HODL invoice: {invoice}"); ``` ### Javascript (Wasm) ```typescript const preimage = '<32-byte unique preimage hex>' const preimageBuffer = Buffer.from(preimage, 'hex') const paymentHash = createHash('sha256').update(preimageBuffer).digest('hex') const response = await sdk.receivePayment({ paymentMethod: { type: 'bolt11Invoice', description: 'HODL invoice', amountSats: 50_000, expirySecs: undefined, paymentHash, receiverIdentityPublicKey: undefined } }) const invoice = response.paymentRequest console.log(`HODL invoice: ${invoice}`) ``` ### React Native ```typescript const preimage = '<32-byte unique preimage hex>' const preimageBuffer = Buffer.from(preimage, 'hex') const paymentHash = createHash('sha256').update(preimageBuffer).digest('hex') const response = await sdk.receivePayment({ paymentMethod: new ReceivePaymentMethod.Bolt11Invoice({ description: 'HODL invoice', amountSats: BigInt(50_000), expirySecs: undefined, paymentHash, receiverIdentityPublicKey: undefined }) }) const invoice = response.paymentRequest console.log(`HODL invoice: ${invoice}`) ``` ### Flutter ```dart String preimage = "<32-byte unique preimage hex>"; List preimageBytes = hex.decode(preimage); Digest paymentHashDigest = sha256.convert(preimageBytes); String paymentHash = hex.encode(paymentHashDigest.bytes); final response = await sdk.receivePayment( request: ReceivePaymentRequest( paymentMethod: ReceivePaymentMethod.bolt11Invoice( description: "HODL invoice", amountSats: BigInt.from(50000), expirySecs: null, paymentHash: paymentHash))); final invoice = response.paymentRequest; print("HODL invoice: $invoice"); ``` ### Python ```python preimage = "<32-byte unique preimage hex>" preimage_bytes = bytes.fromhex(preimage) payment_hash_bytes = hashlib.sha256(preimage_bytes).digest() payment_hash = payment_hash_bytes.hex() response = await sdk.receive_payment( request=ReceivePaymentRequest( payment_method=ReceivePaymentMethod.BOLT11_INVOICE( description="HODL invoice", amount_sats=50_000, expiry_secs=None, payment_hash=payment_hash, receiver_identity_public_key=None, ) ) ) invoice = response.payment_request logging.debug(f"HODL invoice: {invoice}") ``` ### Go ```go preimage := "<32-byte unique preimage hex>" preimageBytes, err := hex.DecodeString(preimage) if err != nil { return err } paymentHashBytes := sha256.Sum256(preimageBytes) paymentHash := hex.EncodeToString(paymentHashBytes[:]) amountSats := uint64(50_000) response, err := sdk.ReceivePayment(breez_sdk_spark.ReceivePaymentRequest{ PaymentMethod: breez_sdk_spark.ReceivePaymentMethodBolt11Invoice{ Description: "HODL invoice", AmountSats: &amountSats, ExpirySecs: nil, PaymentHash: &paymentHash, }, }) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return err } invoice := response.PaymentRequest log.Printf("HODL invoice: %v", invoice) ``` ## Listing claimable conditional payments API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.list_payments Once detected, claimable HTLC payments are immediately listed as pending in the [list of payments](/guide/list_payments.md). Additionally, a `SdkEvent::PaymentPending` event is emitted to notify your application. See [Listening to events](/guide/events.md) for more details. To list only claimable HTLC payments, you can filter by HTLC status. This works for both Spark HTLC payments and HODL invoices. ### Rust ```rust let request = ListPaymentsRequest { type_filter: Some(vec![PaymentType::Receive]), status_filter: Some(vec![PaymentStatus::Pending]), payment_details_filter: Some(vec![ PaymentDetailsFilter::Spark { htlc_status: Some(vec![SparkHtlcStatus::WaitingForPreimage]), conversion_refund_needed: None, }, PaymentDetailsFilter::Lightning { htlc_status: Some(vec![SparkHtlcStatus::WaitingForPreimage]), }, ]), ..Default::default() }; let response = sdk.list_payments(request).await?; let payments = response.payments; for payment in &payments { match &payment.details { Some(PaymentDetails::Spark { htlc_details: Some(htlc), .. }) => { info!("Spark HTLC expiry time: {}", htlc.expiry_time); } Some(PaymentDetails::Lightning { htlc_details: htlc, .. }) => { info!("Lightning HTLC expiry time: {}", htlc.expiry_time); } _ => {} } } ``` ### Swift ```swift let request = ListPaymentsRequest( typeFilter: [PaymentType.receive], statusFilter: [PaymentStatus.pending], paymentDetailsFilter: [ PaymentDetailsFilter.spark( htlcStatus: [SparkHtlcStatus.waitingForPreimage], conversionRefundNeeded: nil ), PaymentDetailsFilter.lightning( htlcStatus: [SparkHtlcStatus.waitingForPreimage] ), ] ) let response = try await sdk.listPayments(request: request) let payments = response.payments for payment in payments { if case let .spark(_, htlcDetails, _) = payment.details, let htlc = htlcDetails { print("Spark HTLC expiry time: \(htlc.expiryTime)") } else if case let .lightning(_, _, _, htlcDetails, _, _, _, _) = payment.details { print("Lightning HTLC expiry time: \(htlcDetails.expiryTime)") } } ``` ### Kotlin ```kotlin try { val request = ListPaymentsRequest( typeFilter = listOf(PaymentType.RECEIVE), statusFilter = listOf(PaymentStatus.PENDING), paymentDetailsFilter = listOf( PaymentDetailsFilter.Spark( htlcStatus = listOf(SparkHtlcStatus.WAITING_FOR_PREIMAGE), conversionRefundNeeded = null ), PaymentDetailsFilter.Lightning( htlcStatus = listOf(SparkHtlcStatus.WAITING_FOR_PREIMAGE) ) ) ) val response = sdk.listPayments(request) val payments = response.payments for (payment in payments) { val details = payment.details when (details) { is PaymentDetails.Spark -> { val htlc = details.htlcDetails if (htlc != null) { // Log.v("Breez", "Spark HTLC expiry time: ${htlc.expiryTime}") } } is PaymentDetails.Lightning -> { val htlc = details.htlcDetails // Log.v("Breez", "Lightning HTLC expiry time: ${htlc.expiryTime}") } else -> {} } } } catch (e: Exception) { // handle error throw e } ``` ### C# ```csharp var request = new ListPaymentsRequest( typeFilter: new PaymentType[] { PaymentType.Receive }, statusFilter: new PaymentStatus[] { PaymentStatus.Pending }, paymentDetailsFilter: new PaymentDetailsFilter[] { new PaymentDetailsFilter.Spark( htlcStatus: new SparkHtlcStatus[] { SparkHtlcStatus.WaitingForPreimage }, conversionRefundNeeded: null ), new PaymentDetailsFilter.Lightning( htlcStatus: new SparkHtlcStatus[] { SparkHtlcStatus.WaitingForPreimage } ) } ); var response = await sdk.ListPayments(request: request); var payments = response.payments; foreach (var payment in payments) { if (payment.details is PaymentDetails.Spark sparkDetails && sparkDetails.htlcDetails != null) { Console.WriteLine($"Spark HTLC expiry time: {sparkDetails.htlcDetails.expiryTime}"); } else if (payment.details is PaymentDetails.Lightning lightningDetails) { Console.WriteLine($"Lightning HTLC expiry time: {lightningDetails.htlcDetails.expiryTime}"); } } ``` ### Javascript (Wasm) ```typescript const response = await sdk.listPayments({ typeFilter: ['receive'], statusFilter: ['pending'], paymentDetailsFilter: [{ type: 'spark', htlcStatus: ['waitingForPreimage'] }, { type: 'lightning', htlcStatus: ['waitingForPreimage'] }], assetFilter: undefined }) const payments = response.payments for (const payment of payments) { if (payment.details?.type === 'spark' && payment.details.htlcDetails != null) { console.log(`Spark HTLC expiry time: ${payment.details.htlcDetails.expiryTime}`) } else if (payment.details?.type === 'lightning') { console.log(`Lightning HTLC expiry time: ${payment.details.htlcDetails.expiryTime}`) } } ``` ### React Native ```typescript const request = { typeFilter: [PaymentType.Receive], statusFilter: [PaymentStatus.Pending], paymentDetailsFilter: [new PaymentDetailsFilter.Spark({ htlcStatus: [SparkHtlcStatus.WaitingForPreimage], conversionRefundNeeded: undefined }), new PaymentDetailsFilter.Lightning({ htlcStatus: [SparkHtlcStatus.WaitingForPreimage] })], assetFilter: undefined, fromTimestamp: undefined, toTimestamp: undefined, offset: undefined, limit: undefined, sortAscending: undefined } const response = await sdk.listPayments(request) const payments = response.payments for (const payment of payments) { if (payment.details?.tag === PaymentDetails_Tags.Spark) { const htlc = payment.details.inner.htlcDetails if (htlc != null) { console.log(`Spark HTLC expiry time: ${htlc.expiryTime}`) } } else if (payment.details?.tag === PaymentDetails_Tags.Lightning) { const htlc = payment.details.inner.htlcDetails console.log(`Lightning HTLC expiry time: ${htlc.expiryTime}`) } } ``` ### Flutter ```dart final request = ListPaymentsRequest( typeFilter: [PaymentType.receive], statusFilter: [PaymentStatus.pending], paymentDetailsFilter: [ PaymentDetailsFilter.spark( htlcStatus: [SparkHtlcStatus.waitingForPreimage], ), PaymentDetailsFilter.lightning( htlcStatus: [SparkHtlcStatus.waitingForPreimage], ), ], ); final response = await sdk.listPayments(request: request); final payments = response.payments; for (final payment in payments) { final details = payment.details; if (details is PaymentDetails_Spark && details.htlcDetails != null) { print("Spark HTLC expiry time: ${details.htlcDetails!.expiryTime}"); } else if (details is PaymentDetails_Lightning) { print("Lightning HTLC expiry time: ${details.htlcDetails.expiryTime}"); } } ``` ### Python ```python request = ListPaymentsRequest( type_filter=[PaymentType.RECEIVE], status_filter=[PaymentStatus.PENDING], payment_details_filter=[ cast(PaymentDetailsFilter, PaymentDetailsFilter.SPARK( htlc_status=[SparkHtlcStatus.WAITING_FOR_PREIMAGE], conversion_refund_needed=None )), cast(PaymentDetailsFilter, PaymentDetailsFilter.LIGHTNING( htlc_status=[SparkHtlcStatus.WAITING_FOR_PREIMAGE], )), ], ) response = await sdk.list_payments(request=request) payments = response.payments for payment in payments: if isinstance(payment.details, PaymentDetails.SPARK): if payment.details.htlc_details is not None: logging.debug(f"Spark HTLC expiry time: {payment.details.htlc_details.expiry_time}") elif isinstance(payment.details, PaymentDetails.LIGHTNING): expiry = payment.details.htlc_details.expiry_time logging.debug(f"Lightning HTLC expiry time: {expiry}") ``` ### Go ```go typeFilter := []breez_sdk_spark.PaymentType{ breez_sdk_spark.PaymentTypeReceive, } statusFilter := []breez_sdk_spark.PaymentStatus{ breez_sdk_spark.PaymentStatusPending, } paymentDetailsFilter := []breez_sdk_spark.PaymentDetailsFilter{ breez_sdk_spark.PaymentDetailsFilterSpark{ HtlcStatus: &[]breez_sdk_spark.SparkHtlcStatus{ breez_sdk_spark.SparkHtlcStatusWaitingForPreimage, }, }, breez_sdk_spark.PaymentDetailsFilterLightning{ HtlcStatus: &[]breez_sdk_spark.SparkHtlcStatus{ breez_sdk_spark.SparkHtlcStatusWaitingForPreimage, }, }, } request := breez_sdk_spark.ListPaymentsRequest{ TypeFilter: &typeFilter, StatusFilter: &statusFilter, PaymentDetailsFilter: &paymentDetailsFilter, } response, err := sdk.ListPayments(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } payments := response.Payments for _, payment := range payments { if payment.Details != nil { switch details := (*payment.Details).(type) { case breez_sdk_spark.PaymentDetailsSpark: if details.HtlcDetails != nil { log.Printf("Spark HTLC expiry time: %v", details.HtlcDetails.ExpiryTime) } case breez_sdk_spark.PaymentDetailsLightning: log.Printf("Lightning HTLC expiry time: %v", details.HtlcDetails.ExpiryTime) } } } ``` ## Claiming conditional payments API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.claim_htlc_payment To claim an HTLC payment, provide the preimage that matches the payment hash. This works for both Spark HTLC payments and HODL invoices. ### Rust ```rust let preimage = "".to_string(); let response = sdk .claim_htlc_payment(ClaimHtlcPaymentRequest { preimage }) .await?; let payment = response.payment; ``` ### Swift ```swift let preimage = "" let response = try await sdk.claimHtlcPayment( request: ClaimHtlcPaymentRequest(preimage: preimage) ) let payment = response.payment ``` ### Kotlin ```kotlin try { val preimage = "" val request = ClaimHtlcPaymentRequest(preimage = preimage) val response = sdk.claimHtlcPayment(request) val payment = response.payment } catch (e: Exception) { // handle error throw e } ``` ### C# ```csharp var preimage = ""; var response = await sdk.ClaimHtlcPayment( request: new ClaimHtlcPaymentRequest(preimage: preimage) ); var payment = response.payment; ``` ### Javascript (Wasm) ```typescript const preimage = '' const response = await sdk.claimHtlcPayment({ preimage }) const payment = response.payment ``` ### React Native ```typescript const preimage = '' const response = await sdk.claimHtlcPayment( { preimage } ) const payment = response.payment ``` ### Flutter ```dart String preimage = ""; final response = await sdk.claimHtlcPayment( request: ClaimHtlcPaymentRequest(preimage: preimage)); final payment = response.payment; ``` ### Python ```python preimage = "" response = await sdk.claim_htlc_payment( request=ClaimHtlcPaymentRequest(preimage=preimage) ) payment = response.payment ``` ### Go ```go preimage := "" request := breez_sdk_spark.ClaimHtlcPaymentRequest{ Preimage: preimage, } response, err := sdk.ClaimHtlcPayment(request) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } payment := response.Payment ``` --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Using an External Signer](/guide/external_signer.md)** # Using an External Signer The External Signer feature allows you to provide custom signing logic for the SDK rather than relying on the SDK's internal key management. This is useful when you want to: - Keep keys in a secured environment - Implement custom key derivation logic - Integrate with existing wallet infrastructure ## Using the Default External Signers The external signer interface is split into two parts: an `ExternalBreezSigner` for SDK-layer signing (LNURL-auth, sync, message signing, ECIES) and an `ExternalSparkSigner` for the Spark wallet flows (transfers, claims, FROST signing, deposits). The SDK also ships a Turnkey-backed implementation that keeps the keys in a secure enclave; see [Using Turnkey](/guide/turnkey.md). The SDK provides a convenient factory function `default_external_signers` that creates both signers from a mnemonic: ### Rust ```rust fn create_signers() -> Result { let mnemonic = "".to_string(); let network = Network::Mainnet; let signers = default_external_signers( mnemonic, None, // passphrase network, Some(0), // account number )?; Ok(signers) } ``` ### Swift ```swift func createSigners() throws -> ExternalSigners { let mnemonic = "" let network = Network.mainnet let signers = try defaultExternalSigners( mnemonic: mnemonic, passphrase: nil, network: network, accountNumber: 0 ) return signers } ``` ### Kotlin ```kotlin fun createSigners(): breez_sdk_spark.ExternalSigners { val mnemonic = "" val network = Network.MAINNET val accountNumber = 0U val signers = defaultExternalSigners( mnemonic = mnemonic, passphrase = null, network = network, accountNumber = accountNumber ) return signers } ``` ### C# ```csharp public static ExternalSigners CreateSigners() { var mnemonic = ""; var network = Network.Mainnet; uint accountNumber = 0; var signers = BreezSdkSparkMethods.DefaultExternalSigners( mnemonic: mnemonic, passphrase: null, network: network, accountNumber: accountNumber ); return signers; } ``` ### Javascript (Wasm) ```typescript const createSigners = () => { const mnemonic = '' const accountNumber = 0 // Create the default signers from the SDK const signers = defaultExternalSigners(mnemonic, null, 'mainnet', accountNumber) return signers } ``` ### React Native ```typescript const createSigners = () => { const mnemonic = '' const accountNumber = 0 // Create the default signers from the SDK const signers = defaultExternalSigners(mnemonic, undefined, Network.Mainnet, accountNumber) return signers } ``` ### Python ```python def create_signers() -> ExternalSigners: mnemonic = "" network = Network.MAINNET account_number = 0 signers = default_external_signers( mnemonic=mnemonic, passphrase=None, network=network, account_number=account_number, ) return signers ``` ### Go ```go func createSigners() (breez_sdk_spark.ExternalSigners, error) { mnemonic := "" network := breez_sdk_spark.NetworkMainnet var accountNumber uint32 = 0 signers, err := breez_sdk_spark.DefaultExternalSigners( mnemonic, nil, // passphrase network, &accountNumber, ) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return breez_sdk_spark.ExternalSigners{}, err } return signers, nil } ``` Provide both signers to the `connect_with_signer` method instead of the regular `connect` method: ### Rust ```rust async fn connect_example(signers: ExternalSigners) -> Result { // Create the config let mut config = default_config(Network::Mainnet); config.api_key = Some("".to_string()); // Connect using the external signers let sdk = connect_with_signer(ConnectWithSignerRequest { config, breez_signer: signers.breez_signer, spark_signer: signers.spark_signer, storage_dir: "./.data".to_string(), }) .await?; Ok(sdk) } ``` ### Swift ```swift func connectExample(signers: ExternalSigners) async throws -> BreezSdk { // Create the config var config = defaultConfig(network: .mainnet) config.apiKey = "" // Connect using the external signers let sdk = try await BreezSdkSpark.connectWithSigner(request: ConnectWithSignerRequest( config: config, breezSigner: signers.breezSigner, sparkSigner: signers.sparkSigner, storageDir: "./.data" )) return sdk } ``` ### Kotlin ```kotlin suspend fun connectWithSigner(signers: breez_sdk_spark.ExternalSigners) { // Create the config val config = defaultConfig(Network.MAINNET) config.apiKey = "" try { // Connect using the external signers val sdk = connectWithSigner(ConnectWithSignerRequest( config = config, breezSigner = signers.breezSigner, sparkSigner = signers.sparkSigner, storageDir = "./.data" )) } catch (e: Exception) { // handle error } } ``` ### C# ```csharp public static async Task ConnectWithSigner(ExternalSigners signers) { // Create the config var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { apiKey = "" }; // Connect using the external signers var sdk = await BreezSdkSparkMethods.ConnectWithSigner(new ConnectWithSignerRequest( config: config, breezSigner: signers.breezSigner, sparkSigner: signers.sparkSigner, storageDir: "./.data" )); return sdk; } ``` ### Javascript (Wasm) ```typescript const exampleConnectWithSigner = async ( signers: ReturnType ) => { // Create the config const config = defaultConfig('mainnet') config.apiKey = '' // Connect using the external signers const sdk = await connectWithSigner( config, signers.breezSigner, signers.sparkSigner, 'breez_spark_db' // For WASM, this is the IndexedDB database name ) } ``` ### React Native ```typescript const exampleConnectWithSigner = async ( signers: ReturnType ) => { // Create the config const config = defaultConfig(Network.Mainnet) config.apiKey = '' // Connect using the external signers const sdk = await connectWithSigner({ config, breezSigner: signers.breezSigner, sparkSigner: signers.sparkSigner, storageDir: `${RNFS.DocumentDirectoryPath}/data` }) } ``` ### Python ```python async def example_connect_with_signer(signers: ExternalSigners) -> BreezSdk: # Create the config config = default_config(Network.MAINNET) config.api_key = "" # Connect using the external signers sdk = await connect_with_signer(ConnectWithSignerRequest( config=config, breez_signer=signers.breez_signer, spark_signer=signers.spark_signer, storage_dir="./.data" )) return sdk ``` ### Go ```go func connectWithSigner( signers breez_sdk_spark.ExternalSigners, ) (*breez_sdk_spark.BreezSdk, error) { // Create the config config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) apiKey := "" config.ApiKey = &apiKey // Connect using the external signers sdk, err := breez_sdk_spark.ConnectWithSigner(breez_sdk_spark.ConnectWithSignerRequest{ Config: config, BreezSigner: signers.BreezSigner, SparkSigner: signers.SparkSigner, StorageDir: "./.data", }) if err != nil { var sdkErr *breez_sdk_spark.SdkError if errors.As(err, &sdkErr) { // Handle SdkError - can inspect specific variants if needed // e.g., switch on sdkErr variant for InsufficientFunds, NetworkError, etc. } return nil, err } return sdk, nil } ``` **Developer note** When using an external signer, you don't provide a seed directly to the SDK. Instead, the signer handles all cryptographic operations internally. ## Advanced Setup with Sdk Builder To compose an external signer along with the options in [customizing the SDK](/guide/customizing.md) (custom storage backends, a shared SDK context, an account number), build the SDK with `new_with_signer` instead. It takes the same two signers and returns an `SdkBuilder` you chain the customization methods on before calling `build`: ### Rust ```rust async fn build_with_signer(signers: ExternalSigners) -> Result { let mut config = default_config(Network::Mainnet); config.api_key = Some("".to_string()); let builder = SdkBuilder::new_with_signer(config, signers.breez_signer, signers.spark_signer); // let builder = builder.with_storage_backend(); // let builder = builder.with_shared_context(); let sdk = builder.build().await?; Ok(sdk) } ``` ### Swift ```swift func buildWithSigner(signers: ExternalSigners) async throws -> BreezSdk { var config = defaultConfig(network: .mainnet) config.apiKey = "" let builder = SdkBuilder.newWithSigner( config: config, breezSigner: signers.breezSigner, sparkSigner: signers.sparkSigner ) // await builder.withStorageBackend(storage: ) // await builder.withSharedContext() let sdk = try await builder.build() return sdk } ``` ### Kotlin ```kotlin suspend fun buildWithSigner(signers: breez_sdk_spark.ExternalSigners) { // Create the config val config = defaultConfig(Network.MAINNET) config.apiKey = "" try { val builder = SdkBuilder.newWithSigner( config, signers.breezSigner, signers.sparkSigner ) // builder.withStorageBackend() // builder.withSharedContext() val sdk = builder.build() } catch (e: Exception) { // handle error } } ``` ### C# ```csharp public static async Task BuildWithSigner(ExternalSigners signers) { var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { apiKey = "" }; var builder = SdkBuilder.NewWithSigner( config: config, breezSigner: signers.breezSigner, sparkSigner: signers.sparkSigner ); // await builder.WithStorageBackend(storage: ); // await builder.WithSharedContext(); var sdk = await builder.Build(); return sdk; } ``` ### Javascript (Wasm) ```typescript const exampleBuildWithSigner = async ( signers: ExternalSigners ): Promise => { const config = defaultConfig('mainnet') config.apiKey = '' const builder = SdkBuilder.newWithSigner( config, signers.breezSigner, signers.sparkSigner ) // builder = builder.withStorageBackend() // builder = builder.withSharedContext() const sdk = await builder.build() return sdk } ``` ### React Native ```typescript const exampleBuildWithSigner = async (signers: ExternalSigners) => { // Create the config const config = defaultConfig(Network.Mainnet) config.apiKey = '' const builder = SdkBuilder.newWithSigner(config, signers.breezSigner, signers.sparkSigner) // await builder.withStorage() // await builder.withAccountNumber() const sdk = await builder.build() } ``` ### Python ```python async def example_build_with_signer(signers: ExternalSigners) -> BreezSdk: config = default_config(Network.MAINNET) config.api_key = "" builder = SdkBuilder.new_with_signer( config=config, breez_signer=signers.breez_signer, spark_signer=signers.spark_signer, ) # await builder.with_storage_backend() # await builder.with_shared_context() sdk = await builder.build() return sdk ``` ### Go ```go func buildWithSigner( signers breez_sdk_spark.ExternalSigners, ) (*breez_sdk_spark.BreezSdk, error) { config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) apiKey := "" config.ApiKey = &apiKey builder := breez_sdk_spark.SdkBuilderNewWithSigner(config, signers.BreezSigner, signers.SparkSigner) // builder.WithStorageBackend() // builder.WithSharedContext() sdk, err := builder.Build() if err != nil { return nil, err } return sdk, nil } ``` For a signer that provides signing only (see [Signers Without Local ECIES/HMAC Support](#signers-without-local-ecieshmac-support)), use `new_with_signing_only_signer` the same way: ### Rust ```rust async fn build_with_signing_only_signer( config: Config, signers: SigningOnlyExternalSigners, ) -> Result { let builder = SdkBuilder::new_with_signing_only_signer( config, signers.breez_signer, signers.spark_signer, ); let sdk = builder.build().await?; Ok(sdk) } ``` ### Swift ```swift func buildWithSigningOnlySigner( config: Config, signers: SigningOnlyExternalSigners ) async throws -> BreezSdk { let builder = SdkBuilder.newWithSigningOnlySigner( config: config, breezSigner: signers.breezSigner, sparkSigner: signers.sparkSigner ) let sdk = try await builder.build() return sdk } ``` ### Kotlin ```kotlin suspend fun buildWithSigningOnlySigner( config: breez_sdk_spark.Config, signers: breez_sdk_spark.SigningOnlyExternalSigners ) { try { val builder = SdkBuilder.newWithSigningOnlySigner( config, signers.breezSigner, signers.sparkSigner ) val sdk = builder.build() } catch (e: Exception) { // handle error } } ``` ### C# ```csharp public static async Task BuildWithSigningOnlySigner(Config config, SigningOnlyExternalSigners signers) { var builder = SdkBuilder.NewWithSigningOnlySigner( config: config, breezSigner: signers.breezSigner, sparkSigner: signers.sparkSigner ); var sdk = await builder.Build(); return sdk; } ``` ### Javascript (Wasm) ```typescript const exampleBuildWithSigningOnlySigner = async ( config: Config, signers: SigningOnlyExternalSigners ): Promise => { const builder = SdkBuilder.newWithSigningOnlySigner( config, signers.breezSigner, signers.sparkSigner ) const sdk = await builder.build() return sdk } ``` ### React Native ```typescript const exampleBuildWithSigningOnlySigner = async ( config: Config, signers: SigningOnlyExternalSigners ) => { const builder = SdkBuilder.newWithSigningOnlySigner( config, signers.breezSigner, signers.sparkSigner ) const sdk = await builder.build() } ``` ### Python ```python async def example_build_with_signing_only_signer( config: Config, signers: SigningOnlyExternalSigners ) -> BreezSdk: builder = SdkBuilder.new_with_signing_only_signer( config=config, breez_signer=signers.breez_signer, spark_signer=signers.spark_signer, ) sdk = await builder.build() return sdk ``` ### Go ```go func buildWithSigningOnlySigner( config breez_sdk_spark.Config, signers breez_sdk_spark.SigningOnlyExternalSigners, ) (*breez_sdk_spark.BreezSdk, error) { builder := breez_sdk_spark.SdkBuilderNewWithSigningOnlySigner(config, signers.BreezSigner, signers.SparkSigner) sdk, err := builder.Build() if err != nil { return nil, err } return sdk, nil } ``` ## Implementing a Custom Signer If you need full control over the signing process, you can implement the [ExternalBreezSigner](https://github.com/breez/spark-sdk/blob/main/crates/breez-sdk/core/src/signer/external.rs) and [ExternalSparkSigner](https://github.com/breez/spark-sdk/blob/main/crates/breez-sdk/core/src/signer/external_spark.rs) interfaces in your application. These interfaces define all the cryptographic operations the SDK needs. The default implementations of the two interfaces, [DefaultExternalSigner](https://github.com/breez/spark-sdk/blob/main/crates/breez-sdk/core/src/signer/default_external.rs) and [DefaultExternalSparkSigner](https://github.com/breez/spark-sdk/blob/main/crates/breez-sdk/core/src/signer/default_external_spark.rs), can be used as a reference for what's expected. **Developer note** Implementing a custom signer requires deep understanding of Bitcoin cryptography. The default signer implementations provide a solid reference for what's expected. Most applications should use the default external signers factory function rather than implementing their own. **Flutter Limitation** External signers are not supported in Flutter due to limitations with passing trait objects through the flutter_rust_bridge FFI. Flutter applications should use the standard `connect` method with mnemonic-based key management. ### Signers Without Local ECIES/HMAC Support Some external signers can't perform the SDK's local ECIES/HMAC operations (for example, a policy-restricted enclave that won't release key material). For these, implement `ExternalSigningSigner` instead of `ExternalBreezSigner`, then connect with `connect_with_signing_only_signer`. With such a signer: - **LNURL-auth** returns an error when called. - **Real-time sync** must be disabled: leave [`real_time_sync_server_url`](/guide/config.md#real-time-sync-server-url) unset, or the build fails. - **Cross-chain** must be disabled: leave [`cross_chain_config`](/guide/config.md#usdc-usdt) unset, or the build fails. --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Managing webhooks](/guide/webhooks.md)** # Managing webhooks Webhooks allow you to receive real-time notifications when events occur in your wallet, such as completed Lightning payments or on-chain deposits. The Spark service provider sends an HTTP POST request to your specified URL whenever a subscribed event occurs. Each webhook payload is signed using HMAC-SHA256 with the secret you provide during registration, allowing you to verify the authenticity of incoming notifications. ## Event types The following event types are available for webhook subscriptions: | Event type | Description | |-----------|-------------| | `WebhookEventType::LightningReceiveFinished` | A Lightning receive operation completed | | `WebhookEventType::LightningSendFinished` | A Lightning send operation completed | | `WebhookEventType::CoopExitFinished` | A cooperative exit completed | | `WebhookEventType::StaticDepositFinished` | A static deposit completed | ## Webhook payload When an event occurs, the Spark service provider sends an HTTP POST request to your webhook URL. The payload is a JSON object whose fields vary by event type. The request includes an `X-Spark-Signature` header containing an HMAC-SHA256 signature of the raw request body, computed using the secret you provided during registration. All payloads share the following common fields: | Field | Type | Description | |-------|------|-------------| | `id` | `string` | Unique identifier for the request | | `created_at` | `string` | ISO 8601 timestamp of when the request was created | | `updated_at` | `string` | ISO 8601 timestamp of the last update | | `network` | `string` | The network (`MAINNET`, `TESTNET`, `REGTEST`) | | `request_status` | `string` | Status of the request (e.g., `COMPLETED`) | | `status` | `string` | Event-specific status | | `type` | `string` | The event type (e.g., `SPARK_LIGHTNING_RECEIVE_FINISHED`) | | `timestamp` | `string` | ISO 8601 timestamp of the event | ### Lightning receive finished ```json { "id": "018677b5-e419-99d1-0000-a7030393c9af", "created_at": "2025-03-09T12:00:00Z", "updated_at": "2025-03-09T12:00:05Z", "network": "MAINNET", "request_status": "COMPLETED", "status": "TRANSFER_COMPLETED", "type": "SPARK_LIGHTNING_RECEIVE_FINISHED", "timestamp": "2025-03-09T12:00:06Z", "payment_preimage": "a1b2c3d4e5f6...", "receiver_identity_public_key": "02abc123...", "invoice_amount": {"value": 50000, "unit": "SATOSHI"}, "htlc_amount": {"value": 50000, "unit": "SATOSHI"} } ``` ### Lightning send finished ```json { "id": "018677b5-e419-99d1-0000-a7030393c9af", "created_at": "2025-03-09T12:00:00Z", "updated_at": "2025-03-09T12:00:05Z", "network": "MAINNET", "request_status": "COMPLETED", "status": "PREIMAGE_PROVIDED", "type": "SPARK_LIGHTNING_SEND_FINISHED", "timestamp": "2025-03-09T12:00:06Z", "encoded_invoice": "lnbc50u1p...", "fee": {"value": 100, "unit": "SATOSHI"}, "idempotency_key": "user-defined-key-123", "invoice_amount": {"value": 50000, "unit": "SATOSHI"} } ``` ### Cooperative exit finished ```json { "id": "018677b5-e419-99d1-0000-a7030393c9af", "created_at": "2025-03-09T12:00:00Z", "updated_at": "2025-03-09T12:00:05Z", "network": "MAINNET", "request_status": "COMPLETED", "status": "SUCCEEDED", "type": "SPARK_COOP_EXIT_FINISHED", "timestamp": "2025-03-09T12:00:06Z", "fee": {"value": 500, "unit": "SATOSHI"}, "withdrawal_address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", "l1_broadcast_fee": {"value": 200, "unit": "SATOSHI"}, "exit_speed": "NORMAL", "coop_exit_txid": "a1b2c3d4...", "expires_at": "2025-03-10T12:00:00Z", "total_amount": {"value": 49300, "unit": "SATOSHI"} } ``` ### Static deposit finished ```json { "id": "018677b5-e419-99d1-0000-a7030393c9af", "created_at": "2025-03-09T12:00:00Z", "updated_at": "2025-03-09T12:00:05Z", "network": "MAINNET", "request_status": "COMPLETED", "status": "TRANSFER_COMPLETED", "type": "SPARK_STATIC_DEPOSIT_FINISHED", "timestamp": "2025-03-09T12:00:06Z", "deposit_amount": {"value": 100000, "unit": "SATOSHI"}, "credit_amount": {"value": 99500, "unit": "SATOSHI"}, "max_fee": {"value": 1000, "unit": "SATOSHI"}, "transaction_id": "d4e5f6a7b8c9...", "output_index": 0, "bitcoin_network": "MAINNET", "static_deposit_address": "bc1q..." } ``` ## Registering a webhook API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.register_webhook To register a webhook, provide a URL, a secret for payload verification, and the event types you want to subscribe to. ### Rust ```rust let response = sdk .register_webhook(RegisterWebhookRequest { url: "https://example.com/webhook".to_string(), secret: "your-webhook-secret".to_string(), event_types: vec![ WebhookEventType::LightningReceiveFinished, WebhookEventType::LightningSendFinished, ], }) .await?; info!("Webhook registered with ID: {}", response.webhook_id); ``` ### Swift ```swift let response = try await sdk.registerWebhook( request: RegisterWebhookRequest( url: "https://example.com/webhook", secret: "your-webhook-secret", eventTypes: [.lightningReceiveFinished, .lightningSendFinished] )) print("Webhook registered with ID: \(response.webhookId)") ``` ### Kotlin ```kotlin val response = sdk.registerWebhook(RegisterWebhookRequest( url = "https://example.com/webhook", secret = "your-webhook-secret", eventTypes = listOf( WebhookEventType.LightningReceiveFinished, WebhookEventType.LightningSendFinished ) )) // Log.v("Breez", "Webhook registered with ID: ${response.webhookId}") ``` ### C# ```csharp var response = await sdk.RegisterWebhook(request: new RegisterWebhookRequest( url: "https://example.com/webhook", secret: "your-webhook-secret", eventTypes: new WebhookEventType[] { new WebhookEventType.LightningReceiveFinished(), new WebhookEventType.LightningSendFinished() } )); Console.WriteLine($"Webhook registered with ID: {response.webhookId}"); ``` ### Javascript (Wasm) ```typescript const response = await sdk.registerWebhook({ url: 'https://example.com/webhook', secret: 'your-webhook-secret', eventTypes: [{ type: 'lightningReceiveFinished' }, { type: 'lightningSendFinished' }] }) console.log(`Webhook registered with ID: ${response.webhookId}`) ``` ### React Native ```typescript const response = await sdk.registerWebhook({ url: 'https://example.com/webhook', secret: 'your-webhook-secret', eventTypes: [ new WebhookEventType.LightningReceiveFinished(), new WebhookEventType.LightningSendFinished() ] }) console.log(`Webhook registered with ID: ${response.webhookId}`) ``` ### Flutter ```dart RegisterWebhookRequest request = RegisterWebhookRequest( url: "https://example.com/webhook", secret: "your-webhook-secret", eventTypes: [ WebhookEventType.lightningReceiveFinished(), WebhookEventType.lightningSendFinished(), ], ); RegisterWebhookResponse response = await sdk.registerWebhook(request: request); print("Webhook registered with ID: ${response.webhookId}"); ``` ### Python ```python event_types = [ WebhookEventType.LIGHTNING_RECEIVE_FINISHED(), WebhookEventType.LIGHTNING_SEND_FINISHED(), ] response = await sdk.register_webhook( request=RegisterWebhookRequest( url="https://example.com/webhook", secret="your-webhook-secret", event_types=event_types, ) ) logging.debug(f"Webhook registered with ID: {response.webhook_id}") ``` ### Go ```go response, err := sdk.RegisterWebhook(breez_sdk_spark.RegisterWebhookRequest{ Url: "https://example.com/webhook", Secret: "your-webhook-secret", EventTypes: []breez_sdk_spark.WebhookEventType{ breez_sdk_spark.WebhookEventTypeLightningReceiveFinished{}, breez_sdk_spark.WebhookEventTypeLightningSendFinished{}, }, }) if err != nil { return nil, err } log.Printf("Webhook registered with ID: %v", response.WebhookId) ``` ## Unregistering a webhook API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.unregister_webhook To stop receiving notifications for a webhook, unregister it using its ID. ### Rust ```rust let webhook_id = "webhook-id".to_string(); sdk.unregister_webhook(UnregisterWebhookRequest { webhook_id }) .await?; info!("Webhook unregistered"); ``` ### Swift ```swift let webhookId = "webhook-id" try await sdk.unregisterWebhook( request: UnregisterWebhookRequest(webhookId: webhookId)) print("Webhook unregistered") ``` ### Kotlin ```kotlin val webhookId = "webhook-id" sdk.unregisterWebhook(UnregisterWebhookRequest(webhookId = webhookId)) // Log.v("Breez", "Webhook unregistered") ``` ### C# ```csharp var webhookId = "webhook-id"; await sdk.UnregisterWebhook(request: new UnregisterWebhookRequest( webhookId: webhookId )); Console.WriteLine("Webhook unregistered"); ``` ### Javascript (Wasm) ```typescript const webhookId = 'webhook-id' await sdk.unregisterWebhook({ webhookId }) console.log('Webhook unregistered') ``` ### React Native ```typescript const webhookId = 'webhook-id' await sdk.unregisterWebhook({ webhookId }) console.log('Webhook unregistered') ``` ### Flutter ```dart String webhookId = "webhook-id"; await sdk.unregisterWebhook( request: UnregisterWebhookRequest(webhookId: webhookId), ); print("Webhook unregistered"); ``` ### Python ```python webhook_id = "webhook-id" await sdk.unregister_webhook( request=UnregisterWebhookRequest(webhook_id=webhook_id) ) logging.debug("Webhook unregistered") ``` ### Go ```go webhookId := "webhook-id" err := sdk.UnregisterWebhook(breez_sdk_spark.UnregisterWebhookRequest{ WebhookId: webhookId, }) if err != nil { return err } log.Printf("Webhook unregistered") ``` ## Listing webhooks API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.list_webhooks To retrieve all currently registered webhooks, use the list method. ### Rust ```rust let webhooks = sdk.list_webhooks().await?; for webhook in webhooks { info!( "Webhook: id={}, url={}, events={:?}", webhook.id, webhook.url, webhook.event_types ); } ``` ### Swift ```swift let webhooks = try await sdk.listWebhooks() for webhook in webhooks { print("Webhook: id=\(webhook.id), url=\(webhook.url), events=\(webhook.eventTypes)") } ``` ### Kotlin ```kotlin val webhooks = sdk.listWebhooks() for (webhook in webhooks) { // Log.v("Breez", "Webhook: id=${webhook.id}, url=${webhook.url}, events=${webhook.eventTypes}") } ``` ### C# ```csharp var webhooks = await sdk.ListWebhooks(); foreach (var webhook in webhooks) { Console.WriteLine($"Webhook: id={webhook.id}, url={webhook.url}, events={webhook.eventTypes}"); } ``` ### Javascript (Wasm) ```typescript const webhooks = await sdk.listWebhooks() for (const webhook of webhooks) { console.log(`Webhook: id=${webhook.id}, url=${webhook.url}, events=${String(webhook.eventTypes)}`) } ``` ### React Native ```typescript const webhooks = await sdk.listWebhooks() for (const webhook of webhooks) { console.log(`Webhook: id=${webhook.id}, url=${webhook.url}, events=${String(webhook.eventTypes)}`) } ``` ### Flutter ```dart List webhooks = await sdk.listWebhooks(); for (Webhook webhook in webhooks) { print("Webhook: id=${webhook.id}, url=${webhook.url}, events=${webhook.eventTypes}"); } ``` ### Python ```python webhooks = await sdk.list_webhooks() for webhook in webhooks: logging.debug( f"Webhook: id={webhook.id}, url={webhook.url}, " f"events={webhook.event_types}" ) ``` ### Go ```go webhooks, err := sdk.ListWebhooks() if err != nil { return nil, err } for _, webhook := range webhooks { log.Printf("Webhook: id=%v, url=%v, events=%v", webhook.Id, webhook.Url, webhook.EventTypes) } ``` --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Server mode](/guide/server_mode.md)** # Server mode API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/fn.default_server_config.html Server mode is the SDK profile for **multi-tenant server deployments** where a single process hosts many wallets and builds an ephemeral SDK instance per request. The SDK is treated as a library: the host orchestrates sync, claiming, and event delivery (typically via webhooks) explicitly, so each per-request SDK stays cheap, predictable, and returns fresh state. Use server mode when: - You run the SDK behind an HTTP/gRPC service that handles many wallets in the same process. - Each request builds the SDK, performs one operation, and disconnects. - Background work that makes sense for a long-lived mobile client (periodic sync, real-time sync, leaf optimization, lightning-address recovery) would be wasted on a per-request lifecycle. If you're building a mobile or desktop wallet, stay on the default ([client mode](/guide/initializing.md)) — server mode disables features your app relies on. When the server must not be able to send payments on its own, server mode pairs with [Client signing](/guide/client_signing.md): the user reviews and signs each payment on their side, and the build and publish steps are stateless, so they fit the per-request lifecycle. ## Selecting server mode Build the config with `default_server_config` instead of `default_config`: ### Rust ```rust // Construct the seed using a mnemonic, entropy or passkey let mnemonic = "".to_string(); let seed = Seed::Mnemonic { mnemonic, passphrase: None, }; // Build a server-mode config: same as default_config(network) with // background_tasks_enabled = false. No periodic sync, no real-time sync // client, no leaf/token optimizer, no flashnet refunder, no lightning- // address recovery, no spark private-mode init. let mut config = default_server_config(Network::Mainnet); config.api_key = Some("".to_string()); // Typically server-mode SDKs are built per request and share infrastructure // (DB pool, REST chain service, SSP/Connection Manager) across instances. // Pass the shared resources via the builder; see the "Customizing the SDK" // page for each component. let sdk = SdkBuilder::new(config, seed) .with_default_storage("./.data".to_string()) .build() .await?; ``` ### Swift ```swift // Construct the seed using a mnemonic, entropy or passkey let mnemonic = "" let seed = Seed.mnemonic(mnemonic: mnemonic, passphrase: nil) // Build a server-mode config: same as defaultConfig(network) with // backgroundTasksEnabled = false. No periodic sync, no real-time sync // client, no leaf/token optimizer, no flashnet refunder, no lightning- // address recovery, no spark private-mode init. var config = defaultServerConfig(network: Network.mainnet) config.apiKey = "" // Typically server-mode SDKs are built per request and share infrastructure // (DB pool, REST chain service, SSP/Connection Manager) across instances. // Pass the shared resources via the builder. let builder = SdkBuilder(config: config, seed: seed) await builder.withDefaultStorage(storageDir: "./.data") let sdk = try await builder.build() ``` ### Kotlin ```kotlin // Construct the seed using a mnemonic, entropy or passkey val mnemonic = "" val seed = Seed.Mnemonic(mnemonic, null) // Build a server-mode config: same as defaultConfig(network) with // backgroundTasksEnabled = false. No periodic sync, no real-time sync // client, no leaf/token optimizer, no flashnet refunder, no lightning- // address recovery, no spark private-mode init. val config = defaultServerConfig(Network.MAINNET) config.apiKey = "" try { // Typically server-mode SDKs are built per request and share // infrastructure (DB pool, REST chain service, SSP/Connection // Manager) across instances. Pass the shared resources via the // builder. val builder = SdkBuilder(config, seed) builder.withDefaultStorage("./.data") val sdk = builder.build() } catch (e: Exception) { // handle error } ``` ### C# ```csharp // Construct the seed using a mnemonic, entropy or passkey var mnemonic = ""; var seed = new Seed.Mnemonic(mnemonic: mnemonic, passphrase: null); // Build a server-mode config: same as DefaultConfig(network) with // backgroundTasksEnabled = false. No periodic sync, no real-time // sync client, no leaf/token optimizer, no flashnet refunder, no // lightning-address recovery, no spark private-mode init. var config = BreezSdkSparkMethods.DefaultServerConfig(Network.Mainnet) with { apiKey = "" }; // Typically server-mode SDKs are built per request and share // infrastructure (DB pool, REST chain service, SSP/Connection // Manager) across instances. Pass the shared resources via the // builder. var builder = new SdkBuilder(config: config, seed: seed); await builder.WithDefaultStorage(storageDir: "./.data"); var sdk = await builder.Build(); ``` ### Javascript (Wasm) ```typescript // Construct the seed using a mnemonic, entropy or passkey const mnemonic = '' const seed: Seed = { type: 'mnemonic', mnemonic, passphrase: undefined } // Build a server-mode config: same as defaultConfig(network) with // backgroundTasksEnabled = false. No periodic sync, no real-time sync // client, no leaf/token optimizer, no flashnet refunder, no lightning- // address recovery, no spark private-mode init. const config = defaultServerConfig('mainnet') config.apiKey = '' // Typically server-mode SDKs are built per request and share infrastructure // (DB pool, REST chain service, SSP/Connection Manager) across instances. // Pass the shared resources via the builder; see the "Customizing the SDK" // page for each component. let builder = SdkBuilder.new(config, seed) builder = await builder.withDefaultStorage('./.data') const sdk = await builder.build() ``` ### React Native ```typescript // Construct the seed using a mnemonic, entropy or passkey const mnemonic = '' const seed = new Seed.Mnemonic({ mnemonic, passphrase: undefined }) // Build a server-mode config: same as defaultConfig(network) with // backgroundTasksEnabled = false. No periodic sync, no real-time sync // client, no leaf/token optimizer, no flashnet refunder, no lightning- // address recovery, no spark private-mode init. const config = defaultServerConfig(Network.Mainnet) config.apiKey = '' // Typically server-mode SDKs are built per request and share infrastructure // (DB pool, REST chain service, SSP/Connection Manager) across instances. // Pass the shared resources via the builder. const builder = new SdkBuilder(config, seed) await builder.withDefaultStorage(`${RNFS.DocumentDirectoryPath}/data`) const sdk = await builder.build() ``` ### Flutter ```dart // Construct the seed using a mnemonic, entropy or passkey String mnemonic = ""; final seed = Seed.mnemonic(mnemonic: mnemonic, passphrase: null); // Build a server-mode config: same as defaultConfig(network) with // backgroundTasksEnabled = false. No periodic sync, no real-time sync // client, no leaf/token optimizer, no flashnet refunder, no lightning- // address recovery, no spark private-mode init. final config = defaultServerConfig(network: Network.mainnet) .copyWith(apiKey: ""); // Typically server-mode SDKs are built per request and share infrastructure // (DB pool, REST chain service, SSP/Connection Manager) across instances. // Pass the shared resources via the builder. final builder = SdkBuilder(config: config, seed: seed); builder.withDefaultStorage(storageDir: "./.data"); final sdk = await builder.build(); ``` ### Python ```python # Construct the seed using a mnemonic, entropy or passkey mnemonic = "" seed = Seed.MNEMONIC(mnemonic=mnemonic, passphrase=None) # Build a server-mode config: same as default_config(network) with # background_tasks_enabled = False. No periodic sync, no real-time sync # client, no leaf/token optimizer, no flashnet refunder, no lightning- # address recovery, no spark private-mode init. config = default_server_config(network=Network.MAINNET) config.api_key = "" try: # Typically server-mode SDKs are built per request and share # infrastructure (DB pool, REST chain service, SSP/Connection Manager) # across instances. Pass the shared resources via the builder. builder = SdkBuilder(config=config, seed=seed) await builder.with_default_storage(storage_dir="./.data") sdk = await builder.build() return sdk except Exception as error: logging.error(error) raise ``` ### Go ```go // Construct the seed using a mnemonic, entropy or passkey mnemonic := "" var seed breez_sdk_spark.Seed = breez_sdk_spark.SeedMnemonic{ Mnemonic: mnemonic, Passphrase: nil, } // Build a server-mode config: same as DefaultConfig(network) with // BackgroundTasksEnabled = false. No periodic sync, no real-time sync // client, no leaf/token optimizer, no flashnet refunder, no lightning- // address recovery, no spark private-mode init. apiKey := "" config := breez_sdk_spark.DefaultServerConfig(breez_sdk_spark.NetworkMainnet) config.ApiKey = &apiKey // Typically server-mode SDKs are built per request and share infrastructure // (DB pool, REST chain service, SSP/Connection Manager) across instances. // Pass the shared resources via the builder. builder := breez_sdk_spark.NewSdkBuilder(config, seed) builder.WithDefaultStorage("./.data") sdk, err := builder.Build() if err != nil { return nil, err } ``` `default_server_config` returns the same `Config` as `default_config` with [`background_tasks_enabled`](/guide/config.md#background-tasks-enabled) set to `false` and the fields whose background services are gated off — [`real_time_sync_server_url`](/guide/config.md#real-time-sync-server-url), [`leaf_optimization_config.auto_enabled`](/guide/config.md#optimization-configuration), and [`token_optimization_config.auto_enabled`](/guide/config.md#optimization-configuration) — reset to their inactive shape. The SDK rejects builds that leave those fields in their active shape while `background_tasks_enabled` is `false`, so prefer this preset over flipping the flag by hand. Server mode usually pairs with **shared infrastructure** across SDK instances. See [Customizing the SDK](/guide/customizing.md) and the [Shared infrastructure](#shared-infrastructure) section below for the exact wiring. ## What server mode turns off None of the following per-instance background work is started when `background_tasks_enabled` is `false`: - **Periodic sync loop** — the SDK does not auto-sync with the Spark network. - **Real-time sync client** — no WebSocket subscription to the [real-time sync server](/guide/config.md#real-time-sync-server-url). - **Spark wallet background processor** — no operator-event subscription, leaf optimizer, or token-output optimizer. - **Unilateral exit data collector** does not run, so the data a [unilateral exit](/guide/unilateral_exit.md) needs is not collected in the background. `sync_wallet` collects it instead; see below. - **Lightning-address recovery** — the SDK does not refresh the registered lightning address on startup. - **Spark private-mode init** — the [`private_enabled_default`](/guide/config.md#private-mode-enabled-by-default) preset is **not** applied automatically on first startup; you must opt in once via `update_user_settings` (see [User settings](/guide/user_settings.md)). - **Flashnet conversion refunder** — no periodic refund pass for failed token conversions. - **Stable Balance** — Stable Balance is not supported in server mode because its conversion worker is a background service. Do not rely on automatic Bitcoin-to-token conversion on receive, activation/deactivation conversion, or other Stable Balance background behavior in this profile. Explicit operations such as `sync_wallet`, `claim_deposit`, `list_unclaimed_deposits`, `refund_deposit`, and `refund_pending_conversions` continue to work and are the intended entry points in this mode. ## Driving the SDK explicitly Because nothing runs in the background, the user is responsible for calling the operations that the SDK would otherwise schedule itself. In practice there are only three things to drive, plus one one-time setup per wallet: ### Sync Call `sync_wallet` **only when an external event tells you the wallet state has changed**. The two common cases: 1. **A webhook fires for an incoming payment** — a Lightning receive completes, an on-chain deposit confirms, an incoming Spark transfer lands. Run `sync_wallet()` from the webhook handler so the wallet picks up the new state before downstream consumers (balance reads, payment lists, etc.) need it. 2. **You explicitly need to reconcile state** — e.g. a periodic reconciliation job for a specific wallet, or a manual admin action. This is rare in practice; the webhook path covers the steady state. **Do not** call `sync_wallet` from user-facing request handlers (e.g. a `GET /balance` endpoint) as a precaution — it's a network round-trip to operators and is not needed if your webhooks are wired up. `get_info` reads from the local tree store directly and is the right primitive for read paths. The `SdkEvent::Synced` event pattern documented in [Listening to events](/guide/events.md) is **not available** in server mode — the SDK has no background subscriber to emit it. Treat `sync_wallet` as the synchronous primitive instead. `sync_wallet` is also what keeps [unilateral exit](/guide/unilateral_exit.md) possible here. The data that makes a leaf exitable without the Spark operators is normally collected by a background task, which does not exist in server mode, so the sync collects it instead: any leaf still missing a chain is attempted before the sync returns. Every sync pays a scan of the stored leaves for this, plus a round trip to the operators on the syncs that find leaves needing one. Turn it off with [`exit_chain_auto_fetch_enabled`](/guide/config.md#unilateral-exit-data) if the wallets you run never need to exit without the operators. ### Claiming on-chain deposits Server-mode SDKs do not run the periodic deposit detection and claim sweep that the mobile profile uses. When your webhook or chain watcher observes a relevant on-chain deposit, handle it explicitly: - Call `sync_wallet` to run the SDK's deposit sync and automatic claim logic using your configured [`max_deposit_claim_fee`](/guide/config.md#max-deposit-claim-fee). - If your backend already knows the deposit outpoint and wants to drive a specific claim, call `claim_deposit` for that `txid`/`vout`. The standard claim flow documented in [Claiming on-chain deposits](/guide/onchain_claims.md) applies. ### Stable Balance Stable Balance is not available in server mode. The feature depends on the client runtime's background conversion worker, so server-mode SDKs will not automatically convert received Bitcoin to the active stable token and will not process Stable Balance activation/deactivation conversions in the background. If [`stable_balance_config`](/guide/config.md#stable-balance-configuration) is set while using server mode, SDK initialization fails with an invalid input error. Explicit token conversion flows used by payment APIs can still be used, but do not configure Stable Balance for a server-mode deployment. ### Token conversion refunds **Only relevant if your deployment uses [token conversions](/guide/token_conversion.md).** If you don't issue or convert tokens, skip this section. The flashnet conversion refunder doesn't run in the background in server mode. If you do use tokens, your host needs to drive `refund_pending_conversions` per affected wallet so failed conversions get refunded. A practical pattern is to track which wallets have pending conversions (e.g. by recording them when a conversion fails) and to run the refund pass for just those wallets on a cadence you control — not to spin up an SDK per wallet every minute regardless. ### One-time setup: Spark private mode The client-mode SDK applies [`private_enabled_default`](/guide/config.md#private-mode-enabled-by-default) on first startup. Server-mode SDKs do not — each per-request SDK would otherwise pay a redundant storage read to check the flag. At provisioning time (when a new wallet is first registered), call `update_user_settings` with `spark_private_mode_enabled` set to `true`. See [User settings](/guide/user_settings.md). ## Event delivery via webhooks Without the background processor, the SDK doesn't emit `PaymentSucceeded` / `PaymentPending` / `ClaimedDeposits` events from operator activity. Deliver those signals through webhooks at your own infrastructure instead: - [Managing webhooks](/guide/webhooks.md) describes the supported event types and registration flow. - [Lightning Address payment notifications](/guide/lnurl_webhooks.md) covers the LNURL server's webhook for incoming LNURL payments. A typical pipeline: webhook arrives → webhook handler builds a per-request SDK, calls `sync_wallet` or the relevant explicit operation (e.g. `claim_deposit`), disconnects. ## Lifecycle pattern There are three distinct shapes for a server-mode interaction, depending on what triggered it. ### User-facing request handlers Generate an invoice, send a payment, list history, etc. **Do not call `sync_wallet` here** — operations that read from local storage (`get_info`, `list_payments`, etc.) do not need a defensive sync, and a network round-trip to operators on every request adds latency without changing the answer. ```text request in ↓ build SDK (default_server_config + shared infra) ↓ do work (receive_payment / send_payment / list_payments / …) ↓ disconnect() ↓ response out ``` #### Rust ```rust // User-facing request handler: do not call sync_wallet here. Operations // that read from local storage (get_info, list_payments, etc.) do not // need a defensive sync. Call sync_wallet only from webhook handlers or // reconciliation jobs that need to observe an external state change. let response = sdk .receive_payment(ReceivePaymentRequest { payment_method: ReceivePaymentMethod::Bolt11Invoice { description: "".to_string(), amount_sats: Some(5_000), expiry_secs: Some(3600), payment_hash: None, receiver_identity_public_key: None, }, }) .await?; // Always disconnect at the end of the request lifecycle to flush // outstanding storage writes. See [Disconnecting](initializing.md). sdk.disconnect().await?; ``` #### Swift ```swift // User-facing request handler: do not call syncWallet here. Operations // that read from local storage (getInfo, listPayments, etc.) do not need // a defensive sync. Call syncWallet only from webhook handlers or // reconciliation jobs that need to observe an external state change. let response = try await sdk.receivePayment( request: ReceivePaymentRequest( paymentMethod: ReceivePaymentMethod.bolt11Invoice( description: "", amountSats: 5_000, expirySecs: 3600, paymentHash: nil, receiverIdentityPublicKey: nil ) )) // Always disconnect at the end of the request lifecycle to flush // outstanding storage writes. try await sdk.disconnect() ``` #### Kotlin ```kotlin // User-facing request handler: do not call syncWallet here. // Operations that read from local storage (getInfo, listPayments, // etc.) do not need a defensive sync. Call syncWallet only from // webhook handlers or reconciliation jobs that need to observe an // external state change. val response = sdk.receivePayment( ReceivePaymentRequest( ReceivePaymentMethod.Bolt11Invoice( "", 5_000.toULong(), 3600.toUInt(), null, null, ) ) ) // Always disconnect at the end of the request lifecycle to flush // outstanding storage writes. sdk.disconnect() ``` #### C# ```csharp // User-facing request handler: do not call SyncWallet here. // Operations that read from local storage (GetInfo, ListPayments, // etc.) do not need a defensive sync. Call SyncWallet only from // webhook handlers or reconciliation jobs that need to observe // an external state change. var paymentMethod = new ReceivePaymentMethod.Bolt11Invoice( description: "", amountSats: 5_000UL, expirySecs: 3600U, paymentHash: null, receiverIdentityPublicKey: null ); var response = await sdk.ReceivePayment( request: new ReceivePaymentRequest(paymentMethod: paymentMethod) ); // Always disconnect at the end of the request lifecycle to flush // outstanding storage writes. await sdk.Disconnect(); ``` #### Javascript (Wasm) ```typescript // User-facing request handler: do not call syncWallet here. Operations // that read from local storage (getInfo, listPayments, etc.) do not need // a defensive sync. Call syncWallet only from webhook handlers or // reconciliation jobs that need to observe an external state change. const response = await sdk.receivePayment({ paymentMethod: { type: 'bolt11Invoice', description: '', amountSats: 5_000, expirySecs: 3600, paymentHash: undefined, receiverIdentityPublicKey: undefined } }) // Always disconnect at the end of the request lifecycle to flush // outstanding storage writes. await sdk.disconnect() ``` #### React Native ```typescript // User-facing request handler: do not call syncWallet here. Operations // that read from local storage (getInfo, listPayments, etc.) do not need // a defensive sync. Call syncWallet only from webhook handlers or // reconciliation jobs that need to observe an external state change. const response = await sdk.receivePayment({ paymentMethod: new ReceivePaymentMethod.Bolt11Invoice({ description: '', amountSats: BigInt(5_000), expirySecs: 3600, paymentHash: undefined, receiverIdentityPublicKey: undefined }) }) // Always disconnect at the end of the request lifecycle to flush // outstanding storage writes. await sdk.disconnect() ``` #### Flutter ```dart // User-facing request handler: do not call syncWallet here. Operations // that read from local storage (getInfo, listPayments, etc.) do not need // a defensive sync. Call syncWallet only from webhook handlers or // reconciliation jobs that need to observe an external state change. final response = await sdk.receivePayment( request: ReceivePaymentRequest( paymentMethod: ReceivePaymentMethod.bolt11Invoice( description: "", amountSats: BigInt.from(5000), expirySecs: 3600, paymentHash: null, ), ), ); // Always disconnect at the end of the request lifecycle to flush // outstanding storage writes. await sdk.disconnect(); ``` #### Python ```python # User-facing request handler: do not call sync_wallet here. Operations # that read from local storage (get_info, list_payments, etc.) do not # need a defensive sync. Call sync_wallet only from webhook handlers or # reconciliation jobs that need to observe an external state change. payment_method = ReceivePaymentMethod.BOLT11_INVOICE( description="", amount_sats=5_000, expiry_secs=3600, payment_hash=None, receiver_identity_public_key=None, ) response = await sdk.receive_payment( request=ReceivePaymentRequest(payment_method=payment_method) ) # Always disconnect at the end of the request lifecycle to flush # outstanding storage writes. await sdk.disconnect() ``` #### Go ```go // User-facing request handler: do not call SyncWallet here. Operations // that read from local storage (GetInfo, ListPayments, etc.) do not need // a defensive sync. Call SyncWallet only from webhook handlers or // reconciliation jobs that need to observe an external state change. amountSats := uint64(5_000) expirySecs := uint32(3600) response, err := sdk.ReceivePayment(breez_sdk_spark.ReceivePaymentRequest{ PaymentMethod: breez_sdk_spark.ReceivePaymentMethodBolt11Invoice{ Description: "", AmountSats: &amountSats, ExpirySecs: &expirySecs, PaymentHash: nil, }, }) if err != nil { return "", err } // Always disconnect at the end of the request lifecycle to flush // outstanding storage writes. if err := sdk.Disconnect(); err != nil { return "", err } ``` ### Webhook handlers and reconciliation jobs Anything driven by an external signal that the wallet state changed. The exact operation depends on the trigger — they're not chained together in the same handler. - **Incoming Lightning / Spark transfer webhook** — call `sync_wallet` so downstream reads see the new payment: ```text webhook in → build SDK → sync_wallet() → disconnect() ``` - **On-chain deposit webhook** (or chain watcher) — call `sync_wallet` to run the deposit sync and automatic claim sweep, or call `claim_deposit` if you want to claim a known outpoint explicitly: ```text webhook in → build SDK → sync_wallet() / claim_deposit() → disconnect() ``` ### One-time provisioning When a wallet is first registered, run a one-time setup pass to apply the configuration the client-mode SDK would otherwise apply itself on first startup — currently the [private mode preset](/guide/config.md#private-mode-enabled-by-default): ```text new wallet registered ↓ build SDK (default_server_config + shared infra) ↓ apply one-time user settings ↓ disconnect() ``` #### Rust ```rust // One-time setup when a wallet is first registered. The client-mode SDK // would normally apply the private-mode preset itself on first startup; // server-mode SDKs do not, so opt in once here via update_user_settings. sdk.update_user_settings(UpdateUserSettingsRequest { spark_private_mode_enabled: Some(true), stable_balance_active_label: None, spark_master_identity_public_key: None, }) .await?; sdk.disconnect().await?; ``` #### Swift ```swift // One-time setup when a wallet is first registered. The client-mode SDK // would normally apply the private-mode preset itself on first startup; // server-mode SDKs do not, so opt in once here via updateUserSettings. try await sdk.updateUserSettings( request: UpdateUserSettingsRequest(sparkPrivateModeEnabled: true)) try await sdk.disconnect() ``` #### Kotlin ```kotlin // One-time setup when a wallet is first registered. The client-mode // SDK would normally apply the private-mode preset itself on first // startup; server-mode SDKs do not, so opt in once here via // updateUserSettings. sdk.updateUserSettings(UpdateUserSettingsRequest(sparkPrivateModeEnabled = true)) sdk.disconnect() ``` #### C# ```csharp // One-time setup when a wallet is first registered. The // client-mode SDK would normally apply the private-mode preset // itself on first startup; server-mode SDKs do not, so opt in // once here via UpdateUserSettings. await sdk.UpdateUserSettings( request: new UpdateUserSettingsRequest(sparkPrivateModeEnabled: true) ); await sdk.Disconnect(); ``` #### Javascript (Wasm) ```typescript // One-time setup when a wallet is first registered. The client-mode SDK // would normally apply the private-mode preset itself on first startup; // server-mode SDKs do not, so opt in once here via updateUserSettings. await sdk.updateUserSettings({ sparkPrivateModeEnabled: true, stableBalanceActiveLabel: undefined }) await sdk.disconnect() ``` #### React Native ```typescript // One-time setup when a wallet is first registered. The client-mode SDK // would normally apply the private-mode preset itself on first startup; // server-mode SDKs do not, so opt in once here via updateUserSettings. await sdk.updateUserSettings({ sparkPrivateModeEnabled: true, stableBalanceActiveLabel: undefined, sparkMasterIdentityPublicKey: undefined }) await sdk.disconnect() ``` #### Flutter ```dart // One-time setup when a wallet is first registered. The client-mode SDK // would normally apply the private-mode preset itself on first startup; // server-mode SDKs do not, so opt in once here via updateUserSettings. await sdk.updateUserSettings( request: UpdateUserSettingsRequest(sparkPrivateModeEnabled: true), ); await sdk.disconnect(); ``` #### Python ```python # One-time setup when a wallet is first registered. The client-mode SDK # would normally apply the private-mode preset itself on first startup; # server-mode SDKs do not, so opt in once here via update_user_settings. await sdk.update_user_settings( request=UpdateUserSettingsRequest(spark_private_mode_enabled=True) ) await sdk.disconnect() ``` #### Go ```go // One-time setup when a wallet is first registered. The client-mode SDK // would normally apply the private-mode preset itself on first startup; // server-mode SDKs do not, so opt in once here via UpdateUserSettings. sparkPrivateModeEnabled := true if err := sdk.UpdateUserSettings(breez_sdk_spark.UpdateUserSettingsRequest{ SparkPrivateModeEnabled: &sparkPrivateModeEnabled, }); err != nil { return err } return sdk.Disconnect() ``` ### A few notes - **Building is cheap when infrastructure is shared.** With the shared chain service, MySQL/Postgres pool, and SSP/Connection Managers configured ([see below](#shared-infrastructure)), each per-request SDK reuses HTTP/2 connections, DB pool slots, and gRPC channels — there's no per-request handshake to operators. - **Always disconnect.** Even though no background loops are running, calling `disconnect` flushes outstanding storage writes and is the documented lifecycle exit. See [Disconnecting](/guide/initializing.md#disconnecting). - **One SDK per request, not one SDK pinned to a worker thread.** The per-request build is fast enough and avoids cross-tenant state leaks. ## Shared infrastructure A server-mode deployment normally pairs the profile with shared resources across every per-request SDK. Each of the following is documented in [Customizing the SDK](/guide/customizing.md): - [PostgreSQL Backend](/guide/customizing.md#with-postgres-backend) — shared DB pool for storage, tree, and token stores. - [MySQL Backend](/guide/customizing.md#with-mysql-backend) — same for MySQL. - [Shared REST Chain Service](/guide/customizing.md#with-shared-rest-chain-service) — one pooled HTTP client instead of one per SDK. - [SSP Connection Manager](/guide/customizing.md#with-ssp-connection-manager) — share the SSP HTTP client across SDKs. - [Connection Manager](/guide/customizing.md#with-connection-manager) — share gRPC channels to the Spark operators across SDKs. Pair `default_server_config` with all of these shared resources — sharing the DB pool, chain service, SSP HTTP client, and gRPC channels across SDKs is the intended deployment shape. ## Driving the `background_tasks_enabled` field directly `default_server_config` is the recommended entry point. If you need to flip the flag on an existing config built another way, see [Background tasks enabled](/guide/config.md#background-tasks-enabled). --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Client signing](/guide/client_signing.md)** # Client signing Client signing lets a server drive payments while the key that approves them stays with the user. The server prepares the payment and builds a small package that describes it, the user reviews and signs the package on their side, and the server publishes it to complete the payment. Use it when the SDK runs on your server, for example hosting wallets for many users, and the server must not be able to send payments on its own. It works for Spark addresses and invoices, Lightning invoices, token payments, Bitcoin addresses and LNURL payments. Client signing is fully opt-in. Without it, `send_payment` works as described in [Sending payments](/guide/send_payment.md). ## How it works 1. **Prepare** on the server with `prepare_send_payment`, exactly as in [Sending payments](/guide/send_payment.md). This validates the input and returns the fees. 2. **Build** on the server with `build_unsigned_transfer_package`. This returns the one item the user needs to sign. It carries the amount, fee and destination of the payment. 3. **Sign** on the user's side. The user reviews the package and signs it with their signer. 4. **Publish** on the server with `publish_signed_transfer_package` to complete the payment. Sometimes the wallet first needs to re-shape its funds so it can send the exact amount (a denomination swap). That swap also needs the user's signature, so it arrives as its own package: publishing it returns `PublishSignedTransferPackageResponse::SwapCompleted`, and you build again from the same prepare response. Repeat until publishing returns `PublishSignedTransferPackageResponse::PaymentSent`. The server keeps no state between these steps. Everything needed to complete the payment travels inside the requests and responses, so building and publishing can happen in different processes or on different instances. This fits [Server mode](/guide/server_mode.md) deployments, where an SDK instance is built per request. ## Signing on the user's side API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/signer/trait.ExternalSparkSigner.html The user's side does not need a connected SDK, only a signer that holds the user's key: any `ExternalSparkSigner` implementation (see [Using an External Signer](/guide/external_signer.md)), whether it runs on the user's device or fronts a remote signing service. The package tells the user exactly what they are approving: the amount, the fee and the destination. Show these to the user before signing. Sign Transfer and Swap packages with `prepare_transfer`, and Token packages with `prepare_token_transaction`: ### Rust ```rust let signature = match &unsigned { UnsignedTransferPackage::Transfer { prepare_transfer, amount_sat, fee_sat, target, } => { // Show the user what they are approving before signing let destination = match target { TransferTarget::Spark { address, .. } => address, TransferTarget::Lightning { bolt11, .. } => bolt11, TransferTarget::CoopExit { address, .. } => address, }; info!("Approve sending {amount_sat} sats (fee {fee_sat} sats) to {destination}"); TransferSignature::Transfer { signed: signer.prepare_transfer(prepare_transfer.clone()).await?, } } UnsignedTransferPackage::Swap { prepare_transfer, amount_sat, fee_sat, .. } => { info!("Approve re-shaping funds for a {amount_sat} sat send (fee {fee_sat} sats)"); TransferSignature::Transfer { signed: signer.prepare_transfer(prepare_transfer.clone()).await?, } } UnsignedTransferPackage::Token { prepare_token_transaction, token_identifier, amount, fee, is_swap, .. } => { if *is_swap { info!("Approve combining token outputs for a {token_identifier} send"); } else { info!("Approve sending {amount} of token {token_identifier} (fee {fee})"); } TransferSignature::Token { signed: signer .prepare_token_transaction(prepare_token_transaction.clone()) .await?, } } UnsignedTransferPackage::TokenBatch { prepare_token_transaction, totals, is_swap, .. } => { if *is_swap { info!("Approve combining token outputs before the batch is sent"); } else { for total in totals { // Unset would mean sats, which a batch cannot send yet let token_id = total.token_identifier.as_deref().unwrap_or_default(); info!("Approve sending {} of token {token_id}", total.amount); } } TransferSignature::Token { signed: signer .prepare_token_transaction(prepare_token_transaction.clone()) .await?, } } }; let signed_package = SignedTransferPackage { unsigned, signature, }; ``` ### Swift ```swift let signature: TransferSignature switch unsigned { case let .transfer(prepareTransfer, amountSat, feeSat, target): // Show the user what they are approving before signing let destination: String switch target { case let .spark(address, _): destination = address case let .lightning(bolt11, _, _, _): destination = bolt11 case let .coopExit(address, _, _): destination = address } print("Approve sending \(amountSat) sats (fee \(feeSat) sats) to \(destination)") signature = TransferSignature.transfer( signed: try await signer.prepareTransfer(request: prepareTransfer) ) case let .swap(prepareTransfer, _, amountSat, feeSat): print("Approve re-shaping funds for a \(amountSat) sat send (fee \(feeSat) sats)") signature = TransferSignature.transfer( signed: try await signer.prepareTransfer(request: prepareTransfer) ) case let .token(prepareTokenTransaction, _, tokenIdentifier, amount, fee, isSwap): if isSwap { print("Approve combining token outputs for a \(tokenIdentifier) send") } else { print("Approve sending \(amount) of token \(tokenIdentifier) (fee \(fee))") } signature = TransferSignature.token( signed: try await signer.prepareTokenTransaction(request: prepareTokenTransaction) ) case let .tokenBatch(prepareTokenTransaction, _, totals, isSwap): if isSwap { print("Approve combining token outputs before the batch is sent") } else { for total in totals { // Unset would mean sats, which a batch cannot send yet let tokenId = total.tokenIdentifier ?? "" print("Approve sending \(total.amount) of token \(tokenId)") } } signature = TransferSignature.token( signed: try await signer.prepareTokenTransaction(request: prepareTokenTransaction) ) } let signedPackage = SignedTransferPackage( unsigned: unsigned, signature: signature ) ``` ### Kotlin ```kotlin val signature = when (unsigned) { is UnsignedTransferPackage.Transfer -> { // Show the user what they are approving before signing val destination = when (val target = unsigned.target) { is TransferTarget.Spark -> target.address is TransferTarget.Lightning -> target.bolt11 is TransferTarget.CoopExit -> target.address } // Log.v("Breez", "Approve sending ${unsigned.amountSat} sats " + // "(fee ${unsigned.feeSat} sats) to $destination") TransferSignature.Transfer( signer.prepareTransfer(unsigned.prepareTransfer) ) } is UnsignedTransferPackage.Swap -> { // Log.v("Breez", "Approve re-shaping funds for a ${unsigned.amountSat} " + // "sat send (fee ${unsigned.feeSat} sats)") TransferSignature.Transfer( signer.prepareTransfer(unsigned.prepareTransfer) ) } is UnsignedTransferPackage.Token -> { if (unsigned.isSwap) { // Log.v("Breez", "Approve combining token outputs for a " + // "${unsigned.tokenIdentifier} send") } else { // Log.v("Breez", "Approve sending ${unsigned.amount} of token " + // "${unsigned.tokenIdentifier} (fee ${unsigned.fee})") } TransferSignature.Token( signer.prepareTokenTransaction(unsigned.prepareTokenTransaction) ) } is UnsignedTransferPackage.TokenBatch -> { if (unsigned.isSwap) { // Log.v("Breez", "Approve combining token outputs before the batch is sent") } else { for (total in unsigned.totals) { // Log.v("Breez", "Approve sending ${total.amount} of token " + // "${total.tokenIdentifier}") } } TransferSignature.Token( signer.prepareTokenTransaction(unsigned.prepareTokenTransaction) ) } } val signedPackage = SignedTransferPackage(unsigned, signature) ``` ### C# ```csharp TransferSignature signature; switch (unsigned) { case UnsignedTransferPackage.Transfer transfer: // Show the user what they are approving before signing var destination = transfer.target switch { TransferTarget.Spark spark => spark.address, TransferTarget.Lightning lightning => lightning.bolt11, TransferTarget.CoopExit coopExit => coopExit.address, _ => throw new Exception("Unknown transfer target") }; Console.WriteLine($"Approve sending {transfer.amountSat} sats " + $"(fee {transfer.feeSat} sats) to {destination}"); signature = new TransferSignature.Transfer( signed: await signer.PrepareTransfer(transfer.prepareTransfer) ); break; case UnsignedTransferPackage.Swap swap: Console.WriteLine("Approve re-shaping funds for a " + $"{swap.amountSat} sat send (fee {swap.feeSat} sats)"); signature = new TransferSignature.Transfer( signed: await signer.PrepareTransfer(swap.prepareTransfer) ); break; case UnsignedTransferPackage.Token token: if (token.isSwap) { Console.WriteLine("Approve combining token outputs for a " + $"{token.tokenIdentifier} send"); } else { Console.WriteLine($"Approve sending {token.amount} of token " + $"{token.tokenIdentifier} (fee {token.fee})"); } signature = new TransferSignature.Token( signed: await signer.PrepareTokenTransaction(token.prepareTokenTransaction) ); break; case UnsignedTransferPackage.TokenBatch tokenBatch: if (tokenBatch.isSwap) { Console.WriteLine("Approve combining token outputs " + "before the batch is sent"); } else { foreach (var total in tokenBatch.totals) { Console.WriteLine($"Approve sending {total.amount} of token " + $"{total.tokenIdentifier}"); } } signature = new TransferSignature.Token( signed: await signer.PrepareTokenTransaction( tokenBatch.prepareTokenTransaction) ); break; default: throw new Exception("Unknown transfer package"); } var signedPackage = new SignedTransferPackage(unsigned: unsigned, signature: signature); ``` ### Javascript (Wasm) ```typescript let signature: TransferSignature switch (unsigned.type) { case 'transfer': { const { prepareTransfer, amountSat, feeSat, target } = unsigned // Show the user what they are approving before signing const destination = target.type === 'lightning' ? target.bolt11 : target.address console.log(`Approve sending ${amountSat} sats (fee ${feeSat} sats) to ${destination}`) signature = { type: 'transfer', signed: await signer.prepareTransfer(prepareTransfer) } break } case 'swap': { const { prepareTransfer, amountSat, feeSat } = unsigned console.log(`Approve re-shaping funds for a ${amountSat} sat send (fee ${feeSat} sats)`) signature = { type: 'transfer', signed: await signer.prepareTransfer(prepareTransfer) } break } case 'token': { const { prepareTokenTransaction, tokenIdentifier, amount, fee, isSwap } = unsigned if (isSwap) { console.log(`Approve combining token outputs for a ${tokenIdentifier} send`) } else { console.log(`Approve sending ${amount} of token ${tokenIdentifier} (fee ${fee})`) } signature = { type: 'token', signed: await signer.prepareTokenTransaction(prepareTokenTransaction) } break } case 'tokenBatch': { const { prepareTokenTransaction, totals, isSwap } = unsigned if (isSwap) { console.log('Approve combining token outputs before the batch is sent') } else { for (const total of totals) { console.log(`Approve sending ${total.amount} of token ${total.tokenIdentifier}`) } } signature = { type: 'token', signed: await signer.prepareTokenTransaction(prepareTokenTransaction) } break } } const signedPackage = { unsigned, signature } ``` ### React Native ```typescript let signature: TransferSignature switch (unsigned.tag) { case UnsignedTransferPackage_Tags.Transfer: { const { prepareTransfer, amountSat, feeSat, target } = unsigned.inner // Show the user what they are approving before signing const destination = target.tag === TransferTarget_Tags.Lightning ? target.inner.bolt11 : target.inner.address console.log(`Approve sending ${amountSat} sats (fee ${feeSat} sats) to ${destination}`) signature = new TransferSignature.Transfer({ signed: await signer.prepareTransfer(prepareTransfer) }) break } case UnsignedTransferPackage_Tags.Swap: { const { prepareTransfer, amountSat, feeSat } = unsigned.inner console.log(`Approve re-shaping funds for a ${amountSat} sat send (fee ${feeSat} sats)`) signature = new TransferSignature.Transfer({ signed: await signer.prepareTransfer(prepareTransfer) }) break } case UnsignedTransferPackage_Tags.Token: { const { prepareTokenTransaction, tokenIdentifier, amount, fee, isSwap } = unsigned.inner if (isSwap) { console.log(`Approve combining token outputs for a ${tokenIdentifier} send`) } else { console.log(`Approve sending ${amount} of token ${tokenIdentifier} (fee ${fee})`) } signature = new TransferSignature.Token({ signed: await signer.prepareTokenTransaction(prepareTokenTransaction) }) break } case UnsignedTransferPackage_Tags.TokenBatch: { const { prepareTokenTransaction, totals, isSwap } = unsigned.inner if (isSwap) { console.log('Approve combining token outputs before the batch is sent') } else { for (const total of totals) { console.log(`Approve sending ${total.amount} of token ${total.tokenIdentifier}`) } } signature = new TransferSignature.Token({ signed: await signer.prepareTokenTransaction(prepareTokenTransaction) }) break } } const signedPackage = { unsigned, signature } ``` ### Flutter ```dart TransferSignature signature; if (unsigned is UnsignedTransferPackage_Transfer) { // Show the user what they are approving before signing final target = unsigned.target; String destination = ""; if (target is TransferTarget_Spark) { destination = target.address; } else if (target is TransferTarget_Lightning) { destination = target.bolt11; } else if (target is TransferTarget_CoopExit) { destination = target.address; } print("Approve sending ${unsigned.amountSat} sats" " (fee ${unsigned.feeSat} sats) to $destination"); signature = TransferSignature.transfer( signed: await signer.prepareTransfer(unsigned.prepareTransfer)); } else if (unsigned is UnsignedTransferPackage_Swap) { print("Approve re-shaping funds for a ${unsigned.amountSat} sat send" " (fee ${unsigned.feeSat} sats)"); signature = TransferSignature.transfer( signed: await signer.prepareTransfer(unsigned.prepareTransfer)); } else if (unsigned is UnsignedTransferPackage_Token) { if (unsigned.isSwap) { print("Approve combining token outputs for a ${unsigned.tokenIdentifier} send"); } else { print("Approve sending ${unsigned.amount} of token" " ${unsigned.tokenIdentifier} (fee ${unsigned.fee})"); } signature = TransferSignature.token( signed: await signer .prepareTokenTransaction(unsigned.prepareTokenTransaction)); } else if (unsigned is UnsignedTransferPackage_TokenBatch) { if (unsigned.isSwap) { print("Approve combining token outputs before the batch is sent"); } else { for (final total in unsigned.totals) { print("Approve sending ${total.amount} of token" " ${total.tokenIdentifier}"); } } signature = TransferSignature.token( signed: await signer .prepareTokenTransaction(unsigned.prepareTokenTransaction)); } else { throw Exception("Unknown transfer package variant"); } final signedPackage = SignedTransferPackage(unsigned: unsigned, signature: signature); ``` ### Python ```python if isinstance(unsigned, UnsignedTransferPackage.TRANSFER): # Show the user what they are approving before signing target = unsigned.target destination = "" if isinstance(target, TransferTarget.SPARK): destination = target.address elif isinstance(target, TransferTarget.LIGHTNING): destination = target.bolt11 elif isinstance(target, TransferTarget.COOP_EXIT): destination = target.address logging.debug( f"Approve sending {unsigned.amount_sat} sats" f" (fee {unsigned.fee_sat} sats) to {destination}" ) signature = TransferSignature.TRANSFER( signed=await signer.prepare_transfer(unsigned.prepare_transfer) ) elif isinstance(unsigned, UnsignedTransferPackage.SWAP): logging.debug( f"Approve re-shaping funds for a {unsigned.amount_sat} sat send" f" (fee {unsigned.fee_sat} sats)" ) signature = TransferSignature.TRANSFER( signed=await signer.prepare_transfer(unsigned.prepare_transfer) ) elif isinstance(unsigned, UnsignedTransferPackage.TOKEN): if unsigned.is_swap: logging.debug( f"Approve combining token outputs for a {unsigned.token_identifier} send" ) else: logging.debug( f"Approve sending {unsigned.amount} of token" f" {unsigned.token_identifier} (fee {unsigned.fee})" ) signature = TransferSignature.TOKEN( signed=await signer.prepare_token_transaction( unsigned.prepare_token_transaction ) ) elif isinstance(unsigned, UnsignedTransferPackage.TOKEN_BATCH): if unsigned.is_swap: logging.debug("Approve combining token outputs before the batch is sent") else: for total in unsigned.totals: logging.debug( f"Approve sending {total.amount} of token {total.token_identifier}" ) signature = TransferSignature.TOKEN( signed=await signer.prepare_token_transaction( unsigned.prepare_token_transaction ) ) else: raise ValueError("Unknown transfer package variant") signed_package = SignedTransferPackage(unsigned=unsigned, signature=signature) ``` ### Go ```go var signature breez_sdk_spark.TransferSignature switch pkg := unsigned.(type) { case breez_sdk_spark.UnsignedTransferPackageTransfer: // Show the user what they are approving before signing var destination string switch target := pkg.Target.(type) { case breez_sdk_spark.TransferTargetSpark: destination = target.Address case breez_sdk_spark.TransferTargetLightning: destination = target.Bolt11 case breez_sdk_spark.TransferTargetCoopExit: destination = target.Address } log.Printf( "Approve sending %v sats (fee %v sats) to %v", pkg.AmountSat, pkg.FeeSat, destination, ) signed, err := signer.PrepareTransfer(pkg.PrepareTransfer) if err != nil { return breez_sdk_spark.SignedTransferPackage{}, err } signature = breez_sdk_spark.TransferSignatureTransfer{Signed: signed} case breez_sdk_spark.UnsignedTransferPackageSwap: log.Printf( "Approve re-shaping funds for a %v sat send (fee %v sats)", pkg.AmountSat, pkg.FeeSat, ) signed, err := signer.PrepareTransfer(pkg.PrepareTransfer) if err != nil { return breez_sdk_spark.SignedTransferPackage{}, err } signature = breez_sdk_spark.TransferSignatureTransfer{Signed: signed} case breez_sdk_spark.UnsignedTransferPackageToken: if pkg.IsSwap { log.Printf( "Approve combining token outputs for a %v send", pkg.TokenIdentifier, ) } else { log.Printf( "Approve sending %v of token %v (fee %v)", pkg.Amount, pkg.TokenIdentifier, pkg.Fee, ) } signed, err := signer.PrepareTokenTransaction(pkg.PrepareTokenTransaction) if err != nil { return breez_sdk_spark.SignedTransferPackage{}, err } signature = breez_sdk_spark.TransferSignatureToken{Signed: signed} case breez_sdk_spark.UnsignedTransferPackageTokenBatch: if pkg.IsSwap { log.Printf("Approve combining token outputs before the batch is sent") } else { for _, total := range pkg.Totals { // Unset would mean sats, which a batch cannot send yet tokenID := "" if total.TokenIdentifier != nil { tokenID = *total.TokenIdentifier } log.Printf( "Approve sending %v of token %s", total.Amount, tokenID, ) } } signed, err := signer.PrepareTokenTransaction(pkg.PrepareTokenTransaction) if err != nil { return breez_sdk_spark.SignedTransferPackage{}, err } signature = breez_sdk_spark.TransferSignatureToken{Signed: signed} } signedPackage := breez_sdk_spark.SignedTransferPackage{ Unsigned: unsigned, Signature: signature, } ``` ## Driving the send from the server API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.build_unsigned_transfer_package Prepare once, then repeat build, sign and publish until the payment is sent: ### Rust ```rust let prepare_response = sdk .prepare_send_payment(PrepareSendPaymentRequest { payment_request: PaymentRequest::Input { input: "".to_string(), }, amount: Some(5_000), token_identifier: None, conversion_options: None, fee_policy: None, }) .await?; loop { let unsigned = sdk .build_unsigned_transfer_package(BuildUnsignedTransferPackageRequest { prepare_response: prepare_response.clone(), options: None, }) .await?; // Send the package to the user, who reviews and signs it let signed_package = sign_package(signer, unsigned).await?; match sdk .publish_signed_transfer_package(PublishSignedTransferPackageRequest { signed_package }) .await? { // The wallet's funds were re-shaped first: build the payment again PublishSignedTransferPackageResponse::SwapCompleted => continue, PublishSignedTransferPackageResponse::PaymentSent { payment } => { return Ok(payment); } // Only a batch package pays several recipients at once PublishSignedTransferPackageResponse::PaymentsSent { .. } => { anyhow::bail!("unexpected batch response for a single payment") } } } ``` ### Swift ```swift let prepareResponse = try await sdk.prepareSendPayment( request: PrepareSendPaymentRequest( paymentRequest: .input(input: ""), amount: BInt(5_000), tokenIdentifier: nil, conversionOptions: nil, feePolicy: nil )) while true { let unsigned = try await sdk.buildUnsignedTransferPackage( request: BuildUnsignedTransferPackageRequest( prepareResponse: prepareResponse, options: nil )) // Send the package to the user, who reviews and signs it let signedPackage = try await signPackage(signer: signer, unsigned: unsigned) let publishResponse = try await sdk.publishSignedTransferPackage( request: PublishSignedTransferPackageRequest(signedPackage: signedPackage)) switch publishResponse { // The wallet's funds were re-shaped first: build the payment again case .swapCompleted: continue case let .paymentSent(payment): return payment // Only a batch package pays several recipients at once case .paymentsSent: throw SdkError.InvalidInput("unexpected batch response for a single payment") } } ``` ### Kotlin ```kotlin val prepareResponse = sdk.prepareSendPayment( PrepareSendPaymentRequest( paymentRequest = PaymentRequest.Input(input = ""), amount = BigInteger.fromLong(5_000L), tokenIdentifier = null, conversionOptions = null, feePolicy = null, ) ) while (true) { val unsigned = sdk.buildUnsignedTransferPackage( BuildUnsignedTransferPackageRequest( prepareResponse = prepareResponse, options = null, ) ) // Send the package to the user, who reviews and signs it val signedPackage = signPackage(signer, unsigned) val result = sdk.publishSignedTransferPackage( PublishSignedTransferPackageRequest(signedPackage) ) when (result) { // The wallet's funds were re-shaped first: build the payment again is PublishSignedTransferPackageResponse.SwapCompleted -> continue is PublishSignedTransferPackageResponse.PaymentSent -> return result.payment // Only a batch package pays several recipients at once is PublishSignedTransferPackageResponse.PaymentsSent -> throw IllegalStateException("unexpected batch response for a single payment") } } ``` ### C# ```csharp var prepareResponse = await sdk.PrepareSendPayment( request: new PrepareSendPaymentRequest( paymentRequest: new PaymentRequest.Input(input: ""), amount: 5_000UL, tokenIdentifier: null, conversionOptions: null, feePolicy: null ) ); while (true) { var unsigned = await sdk.BuildUnsignedTransferPackage( request: new BuildUnsignedTransferPackageRequest( prepareResponse: prepareResponse, options: null ) ); // Send the package to the user, who reviews and signs it var signedPackage = await SignPackage(signer, unsigned); var response = await sdk.PublishSignedTransferPackage( request: new PublishSignedTransferPackageRequest(signedPackage: signedPackage) ); switch (response) { // The wallet's funds were re-shaped first: build the payment again case PublishSignedTransferPackageResponse.SwapCompleted: continue; case PublishSignedTransferPackageResponse.PaymentSent paymentSent: return paymentSent.payment; // Only a batch package pays several recipients at once case PublishSignedTransferPackageResponse.PaymentsSent: throw new Exception("unexpected batch response for a single payment"); } } ``` ### Javascript (Wasm) ```typescript const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: { type: 'input', input: '' }, amount: BigInt(5_000), tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: undefined }) while (true) { const unsigned = await sdk.buildUnsignedTransferPackage({ prepareResponse, options: undefined }) // Send the package to the user, who reviews and signs it const signedPackage = await signPackage(signer, unsigned) const publishResponse = await sdk.publishSignedTransferPackage({ signedPackage }) if (publishResponse.type === 'swapCompleted') { // The wallet's funds were re-shaped first: build the payment again continue } // Only a batch package pays several recipients at once if (publishResponse.type === 'paymentsSent') { throw new Error('unexpected batch response for a single payment') } return publishResponse.payment } ``` ### React Native ```typescript const prepareResponse = await sdk.prepareSendPayment({ paymentRequest: new PaymentRequest.Input({ input: '' }), amount: BigInt(5_000), tokenIdentifier: undefined, conversionOptions: undefined, feePolicy: undefined }) while (true) { const unsigned = await sdk.buildUnsignedTransferPackage({ prepareResponse, options: undefined }) // Send the package to the user, who reviews and signs it const signedPackage = await signPackage(signer, unsigned) const publishResponse = await sdk.publishSignedTransferPackage({ signedPackage }) if (publishResponse.tag === PublishSignedTransferPackageResponse_Tags.SwapCompleted) { // The wallet's funds were re-shaped first: build the payment again continue } // Only a batch package pays several recipients at once if (publishResponse.tag === PublishSignedTransferPackageResponse_Tags.PaymentsSent) { throw new Error('unexpected batch response for a single payment') } return publishResponse.inner.payment } ``` ### Flutter ```dart final prepareResponse = await sdk.prepareSendPayment( request: PrepareSendPaymentRequest( paymentRequest: PaymentRequest.input(input: ""), amount: BigInt.from(5000), tokenIdentifier: null, conversionOptions: null, feePolicy: null)); while (true) { final unsigned = await sdk.buildUnsignedTransferPackage( request: BuildUnsignedTransferPackageRequest( prepareResponse: prepareResponse, options: null)); // Send the package to the user, who reviews and signs it final signedPackage = await signPackage(signer, unsigned); final result = await sdk.publishSignedTransferPackage( request: PublishSignedTransferPackageRequest(signedPackage: signedPackage)); if (result is PublishSignedTransferPackageResponse_SwapCompleted) { // The wallet's funds were re-shaped first: build the payment again continue; } if (result is PublishSignedTransferPackageResponse_PaymentSent) { return result.payment; } // Only a batch package pays several recipients at once if (result is PublishSignedTransferPackageResponse_PaymentsSent) { throw Exception("unexpected batch response for a single payment"); } } ``` ### Python ```python try: prepare_response = await sdk.prepare_send_payment( PrepareSendPaymentRequest( payment_request=PaymentRequest.INPUT(input=""), amount=5_000, token_identifier=None, conversion_options=None, fee_policy=None, ) ) while True: unsigned = await sdk.build_unsigned_transfer_package( BuildUnsignedTransferPackageRequest( prepare_response=prepare_response, options=None ) ) # Send the package to the user, who reviews and signs it signed_package = await sign_package(signer, unsigned) result = await sdk.publish_signed_transfer_package( PublishSignedTransferPackageRequest(signed_package=signed_package) ) if isinstance(result, PublishSignedTransferPackageResponse.SWAP_COMPLETED): # The wallet's funds were re-shaped first: build the payment again continue if isinstance(result, PublishSignedTransferPackageResponse.PAYMENT_SENT): return result.payment # Only a batch package pays several recipients at once if isinstance(result, PublishSignedTransferPackageResponse.PAYMENTS_SENT): raise ValueError("unexpected batch response for a single payment") except Exception as error: logging.error(error) raise ``` ### Go ```go paymentRequest := "" amountSats := new(big.Int).SetInt64(5_000) prepareResponse, err := sdk.PrepareSendPayment(breez_sdk_spark.PrepareSendPaymentRequest{ PaymentRequest: breez_sdk_spark.PaymentRequestInput{Input: paymentRequest}, Amount: &amountSats, TokenIdentifier: nil, ConversionOptions: nil, FeePolicy: nil, }) if err != nil { return nil, err } for { unsigned, err := sdk.BuildUnsignedTransferPackage( breez_sdk_spark.BuildUnsignedTransferPackageRequest{ PrepareResponse: prepareResponse, Options: nil, }, ) if err != nil { return nil, err } // Send the package to the user, who reviews and signs it signedPackage, err := SignPackage(signer, unsigned) if err != nil { return nil, err } response, err := sdk.PublishSignedTransferPackage( breez_sdk_spark.PublishSignedTransferPackageRequest{ SignedPackage: signedPackage, }, ) if err != nil { return nil, err } switch result := response.(type) { // The wallet's funds were re-shaped first: build the payment again case breez_sdk_spark.PublishSignedTransferPackageResponseSwapCompleted: continue case breez_sdk_spark.PublishSignedTransferPackageResponsePaymentSent: return &result.Payment, nil // Only a batch package pays several recipients at once case breez_sdk_spark.PublishSignedTransferPackageResponsePaymentsSent: return nil, errors.New("unexpected batch response for a single payment") } } ``` ### Bitcoin For Bitcoin addresses, choose the confirmation speed when building the package. The fee, and therefore what the user signs, depends on it: #### Rust ```rust // For Bitcoin address sends, the confirmation speed is chosen when // building the package: the fee depends on it let unsigned = sdk .build_unsigned_transfer_package(BuildUnsignedTransferPackageRequest { prepare_response, options: Some(BuildTransferPackageOptions::BitcoinAddress { confirmation_speed: OnchainConfirmationSpeed::Medium, }), }) .await?; ``` #### Swift ```swift // For Bitcoin address sends, the confirmation speed is chosen when // building the package: the fee depends on it let unsigned = try await sdk.buildUnsignedTransferPackage( request: BuildUnsignedTransferPackageRequest( prepareResponse: prepareResponse, options: BuildTransferPackageOptions.bitcoinAddress( confirmationSpeed: OnchainConfirmationSpeed.medium ) )) ``` #### Kotlin ```kotlin // For Bitcoin address sends, the confirmation speed is chosen when // building the package: the fee depends on it val unsigned = sdk.buildUnsignedTransferPackage( BuildUnsignedTransferPackageRequest( prepareResponse = prepareResponse, options = BuildTransferPackageOptions.BitcoinAddress( confirmationSpeed = OnchainConfirmationSpeed.MEDIUM ), ) ) ``` #### C# ```csharp // For Bitcoin address sends, the confirmation speed is chosen when // building the package: the fee depends on it var unsigned = await sdk.BuildUnsignedTransferPackage( request: new BuildUnsignedTransferPackageRequest( prepareResponse: prepareResponse, options: new BuildTransferPackageOptions.BitcoinAddress( confirmationSpeed: OnchainConfirmationSpeed.Medium ) ) ); ``` #### Javascript (Wasm) ```typescript // For Bitcoin address sends, the confirmation speed is chosen when // building the package: the fee depends on it const unsigned = await sdk.buildUnsignedTransferPackage({ prepareResponse, options: { type: 'bitcoinAddress', confirmationSpeed: 'medium' } }) ``` #### React Native ```typescript // For Bitcoin address sends, the confirmation speed is chosen when // building the package: the fee depends on it const unsigned = await sdk.buildUnsignedTransferPackage({ prepareResponse, options: new BuildTransferPackageOptions.BitcoinAddress({ confirmationSpeed: OnchainConfirmationSpeed.Medium }) }) ``` #### Flutter ```dart // For Bitcoin address sends, the confirmation speed is chosen when // building the package: the fee depends on it final unsigned = await sdk.buildUnsignedTransferPackage( request: BuildUnsignedTransferPackageRequest( prepareResponse: prepareResponse, options: BuildTransferPackageOptions.bitcoinAddress( confirmationSpeed: OnchainConfirmationSpeed.medium))); ``` #### Python ```python # For Bitcoin address sends, the confirmation speed is chosen when # building the package: the fee depends on it try: unsigned = await sdk.build_unsigned_transfer_package( BuildUnsignedTransferPackageRequest( prepare_response=prepare_response, options=BuildTransferPackageOptions.BITCOIN_ADDRESS( confirmation_speed=OnchainConfirmationSpeed.MEDIUM ), ) ) except Exception as error: logging.error(error) raise ``` #### Go ```go // For Bitcoin address sends, the confirmation speed is chosen when // building the package: the fee depends on it var options breez_sdk_spark.BuildTransferPackageOptions options = breez_sdk_spark.BuildTransferPackageOptionsBitcoinAddress{ ConfirmationSpeed: breez_sdk_spark.OnchainConfirmationSpeedMedium, } unsigned, err := sdk.BuildUnsignedTransferPackage( breez_sdk_spark.BuildUnsignedTransferPackageRequest{ PrepareResponse: prepareResponse, Options: &options, }, ) if err != nil { return err } ``` ### Lightning For BOLT11 invoices the build options work like the send options in [Sending payments](/guide/send_payment.md#lightning-1): `prefer_spark` sends via a direct Spark transfer when the invoice also contains a Spark address, and `completion_timeout_secs` controls how long publishing waits for the payment to complete before returning it while still pending: #### Rust ```rust let unsigned = sdk .build_unsigned_transfer_package(BuildUnsignedTransferPackageRequest { prepare_response, options: Some(BuildTransferPackageOptions::Bolt11Invoice { prefer_spark: true, completion_timeout_secs: Some(10), }), }) .await?; ``` #### Swift ```swift let unsigned = try await sdk.buildUnsignedTransferPackage( request: BuildUnsignedTransferPackageRequest( prepareResponse: prepareResponse, options: BuildTransferPackageOptions.bolt11Invoice( preferSpark: true, completionTimeoutSecs: 10 ) )) ``` #### Kotlin ```kotlin val unsigned = sdk.buildUnsignedTransferPackage( BuildUnsignedTransferPackageRequest( prepareResponse = prepareResponse, options = BuildTransferPackageOptions.Bolt11Invoice( preferSpark = true, completionTimeoutSecs = 10u, ), ) ) ``` #### C# ```csharp var unsigned = await sdk.BuildUnsignedTransferPackage( request: new BuildUnsignedTransferPackageRequest( prepareResponse: prepareResponse, options: new BuildTransferPackageOptions.Bolt11Invoice( preferSpark: true, completionTimeoutSecs: 10 ) ) ); ``` #### Javascript (Wasm) ```typescript const unsigned = await sdk.buildUnsignedTransferPackage({ prepareResponse, options: { type: 'bolt11Invoice', preferSpark: true, completionTimeoutSecs: 10 } }) ``` #### React Native ```typescript const unsigned = await sdk.buildUnsignedTransferPackage({ prepareResponse, options: new BuildTransferPackageOptions.Bolt11Invoice({ preferSpark: true, completionTimeoutSecs: 10 }) }) ``` #### Flutter ```dart final unsigned = await sdk.buildUnsignedTransferPackage( request: BuildUnsignedTransferPackageRequest( prepareResponse: prepareResponse, options: BuildTransferPackageOptions.bolt11Invoice( preferSpark: true, completionTimeoutSecs: 10))); ``` #### Python ```python try: unsigned = await sdk.build_unsigned_transfer_package( BuildUnsignedTransferPackageRequest( prepare_response=prepare_response, options=BuildTransferPackageOptions.BOLT11_INVOICE( prefer_spark=True, completion_timeout_secs=10 ), ) ) except Exception as error: logging.error(error) raise ``` #### Go ```go var completionTimeoutSecs uint32 = 10 var options breez_sdk_spark.BuildTransferPackageOptions options = breez_sdk_spark.BuildTransferPackageOptionsBolt11Invoice{ PreferSpark: true, CompletionTimeoutSecs: &completionTimeoutSecs, } unsigned, err := sdk.BuildUnsignedTransferPackage( breez_sdk_spark.BuildUnsignedTransferPackageRequest{ PrepareResponse: prepareResponse, Options: &options, }, ) if err != nil { return err } ``` ### Tokens Token payments follow the same loop. Prepare with a token identifier as in [Token payments](/guide/token_payments.md). The package amounts are in the token's base units, and the user signs with `prepare_token_transaction`. A Token package with `is_swap` set means the wallet first needs to combine token outputs: publishing it returns `PublishSignedTransferPackageResponse::SwapCompleted`, just like the Bitcoin case. ## LNURL-Pay API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/struct.BreezSdk.html#method.build_unsigned_lnurl_pay_package LNURL payments have their own pair of methods, because completing them includes the LNURL exchange with the recipient's service. Prepare with `prepare_lnurl_pay` as in [LNURL-Pay](/guide/lnurl_pay.md), then run the same loop with `build_unsigned_lnurl_pay_package` and `publish_signed_lnurl_pay_package`. The result carries the LNURL response, including any success action: ### Rust ```rust loop { let unsigned = sdk .build_unsigned_lnurl_pay_package(BuildUnsignedLnurlPayPackageRequest { prepare_response: prepare_response.clone(), }) .await?; let signed_package = sign_package(signer, unsigned).await?; match sdk .publish_signed_lnurl_pay_package(PublishSignedLnurlPayPackageRequest { signed_package, }) .await? { PublishSignedLnurlPayResponse::SwapCompleted => continue, PublishSignedLnurlPayResponse::PaymentSent { response } => { return Ok(response); } } } ``` ### Swift ```swift while true { let unsigned = try await sdk.buildUnsignedLnurlPayPackage( request: BuildUnsignedLnurlPayPackageRequest( prepareResponse: prepareResponse )) let signedPackage = try await signPackage(signer: signer, unsigned: unsigned) let publishResponse = try await sdk.publishSignedLnurlPayPackage( request: PublishSignedLnurlPayPackageRequest(signedPackage: signedPackage)) switch publishResponse { case .swapCompleted: continue case let .paymentSent(response): return response } } ``` ### Kotlin ```kotlin while (true) { val unsigned = sdk.buildUnsignedLnurlPayPackage( BuildUnsignedLnurlPayPackageRequest(prepareResponse) ) val signedPackage = signPackage(signer, unsigned) val result = sdk.publishSignedLnurlPayPackage( PublishSignedLnurlPayPackageRequest(signedPackage) ) when (result) { is PublishSignedLnurlPayResponse.SwapCompleted -> continue is PublishSignedLnurlPayResponse.PaymentSent -> return result.response } } ``` ### C# ```csharp while (true) { var unsigned = await sdk.BuildUnsignedLnurlPayPackage( request: new BuildUnsignedLnurlPayPackageRequest( prepareResponse: prepareResponse ) ); var signedPackage = await SignPackage(signer, unsigned); var response = await sdk.PublishSignedLnurlPayPackage( request: new PublishSignedLnurlPayPackageRequest(signedPackage: signedPackage) ); switch (response) { case PublishSignedLnurlPayResponse.SwapCompleted: continue; case PublishSignedLnurlPayResponse.PaymentSent paymentSent: return paymentSent.response; } } ``` ### Javascript (Wasm) ```typescript while (true) { const unsigned = await sdk.buildUnsignedLnurlPayPackage({ prepareResponse }) const signedPackage = await signPackage(signer, unsigned) const publishResponse = await sdk.publishSignedLnurlPayPackage({ signedPackage }) if (publishResponse.type === 'swapCompleted') { continue } return publishResponse.response } ``` ### React Native ```typescript while (true) { const unsigned = await sdk.buildUnsignedLnurlPayPackage({ prepareResponse }) const signedPackage = await signPackage(signer, unsigned) const publishResponse = await sdk.publishSignedLnurlPayPackage({ signedPackage }) if (publishResponse.tag === PublishSignedLnurlPayResponse_Tags.SwapCompleted) { continue } return publishResponse.inner.response } ``` ### Flutter ```dart while (true) { final unsigned = await sdk.buildUnsignedLnurlPayPackage( request: BuildUnsignedLnurlPayPackageRequest(prepareResponse: prepareResponse)); final signedPackage = await signPackage(signer, unsigned); final result = await sdk.publishSignedLnurlPayPackage( request: PublishSignedLnurlPayPackageRequest(signedPackage: signedPackage)); if (result is PublishSignedLnurlPayResponse_SwapCompleted) { continue; } if (result is PublishSignedLnurlPayResponse_PaymentSent) { return result.response; } } ``` ### Python ```python try: while True: unsigned = await sdk.build_unsigned_lnurl_pay_package( BuildUnsignedLnurlPayPackageRequest(prepare_response=prepare_response) ) signed_package = await sign_package(signer, unsigned) result = await sdk.publish_signed_lnurl_pay_package( PublishSignedLnurlPayPackageRequest(signed_package=signed_package) ) if isinstance(result, PublishSignedLnurlPayResponse.SWAP_COMPLETED): continue if isinstance(result, PublishSignedLnurlPayResponse.PAYMENT_SENT): return result.response except Exception as error: logging.error(error) raise ``` ### Go ```go for { unsigned, err := sdk.BuildUnsignedLnurlPayPackage( breez_sdk_spark.BuildUnsignedLnurlPayPackageRequest{ PrepareResponse: prepareResponse, }, ) if err != nil { return nil, err } signedPackage, err := SignPackage(signer, unsigned) if err != nil { return nil, err } response, err := sdk.PublishSignedLnurlPayPackage( breez_sdk_spark.PublishSignedLnurlPayPackageRequest{ SignedPackage: signedPackage, }, ) if err != nil { return nil, err } switch result := response.(type) { case breez_sdk_spark.PublishSignedLnurlPayResponseSwapCompleted: continue case breez_sdk_spark.PublishSignedLnurlPayResponsePaymentSent: return &result.Response, nil } } ``` ## Failures and retries - Publishing the same signed package twice returns the same result, so it is safe to retry after a lost response or a network error. - If publishing fails because the wallet's funds moved or fees changed since the package was built, prepare again and restart the loop with a fresh package. - Never reuse a signature for a changed payment. Any change to the amount, fee or destination needs a new package, reviewed and signed by the user. ## Remote signers The signature does not have to come from a device holding the mnemonic. Any `ExternalSparkSigner` implementation can sign the package, including one backed by a remote signing service. With Turnkey, a policy can require the end user to approve the transfer signing while the server runs the rest; see [Using Turnkey](/guide/turnkey.md#user-approved-payments). ## Limitations - Payments with a conversion step (see [Converting tokens](/guide/token_conversion.md)) are not supported. - USDC/USDT cross-chain sends are not supported. --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Using Turnkey](/guide/turnkey.md)** # Using Turnkey [Turnkey](https://www.turnkey.com/) keeps the wallet's keys inside a secure enclave. The SDK ships Turnkey-backed signers, so a server can run wallets without holding any signing key material: signing happens inside Turnkey, and what the server holds is an API credential whose permissions you control with Turnkey policies. Turnkey is meant for server deployments (see [Server mode](/guide/server_mode.md)). Depending on the policy you attach to the server's credential, it supports two ways of sending payments: the server signs everything itself, or each payment is approved by the end user via [Client signing](/guide/client_signing.md). The SDK connects to an existing Spark wallet in your Turnkey organization or sub-organization. Creating the wallet itself is done with Turnkey directly and is out of the SDK's scope (see [Turnkey's Spark support](https://docs.turnkey.com/features/networks/spark) for creating the wallet account). ## Connecting API docs: https://breez.github.io/spark-sdk/breez_sdk_spark/turnkey/fn.create_turnkey_signer.html Create the signers with `create_turnkey_signer` and connect with `connect_with_signer`, the same way as any [external signer](/guide/external_signer.md): ### Rust ```rust let turnkey_config = TurnkeyConfig { base_url: None, organization_id: "".to_string(), api_public_key: "".to_string(), api_private_key: "".to_string(), wallet_id: "".to_string(), network: Network::Mainnet, account_number: None, // Set after the first connect to make later signer setup network-free identity_public_key: None, retry: None, max_rps: None, proxy: None, }; let signers = create_turnkey_signer(turnkey_config).await?; let mut config = default_config(Network::Mainnet); config.api_key = Some("".to_string()); let sdk = connect_with_signer(ConnectWithSignerRequest { config, breez_signer: signers.breez_signer, spark_signer: signers.spark_signer, storage_dir: "./.data".to_string(), }) .await?; ``` ### Swift ```swift let turnkeyConfig = TurnkeyConfig( baseUrl: nil, organizationId: "", apiPublicKey: "", apiPrivateKey: "", walletId: "", network: Network.mainnet, accountNumber: nil, // Set after the first connect to make later signer setup network-free identityPublicKey: nil, retry: nil, maxRps: nil ) let signers = try await createTurnkeySigner(config: turnkeyConfig) var config = defaultConfig(network: Network.mainnet) config.apiKey = "" let sdk = try await BreezSdkSpark.connectWithSigner(request: ConnectWithSignerRequest( config: config, breezSigner: signers.breezSigner, sparkSigner: signers.sparkSigner, storageDir: "./.data" )) ``` ### Kotlin ```kotlin val turnkeyConfig = TurnkeyConfig( baseUrl = null, organizationId = "", apiPublicKey = "", apiPrivateKey = "", walletId = "", network = Network.MAINNET, accountNumber = null, // Set after the first connect to make later signer setup network-free identityPublicKey = null, retry = null, maxRps = null ) try { val signers = createTurnkeySigner(turnkeyConfig) val config = defaultConfig(Network.MAINNET) config.apiKey = "" val sdk = connectWithSigner(ConnectWithSignerRequest( config = config, breezSigner = signers.breezSigner, sparkSigner = signers.sparkSigner, storageDir = "./.data" )) } catch (e: Exception) { // handle error } ``` ### C# ```csharp var turnkeyConfig = new TurnkeyConfig( baseUrl: null, organizationId: "", apiPublicKey: "", apiPrivateKey: "", walletId: "", network: Network.Mainnet, accountNumber: null, // Set after the first connect to make later signer setup network-free identityPublicKey: null, retry: null, maxRps: null ); var signers = await BreezSdkSparkMethods.CreateTurnkeySigner(turnkeyConfig); var config = BreezSdkSparkMethods.DefaultConfig(Network.Mainnet) with { apiKey = "" }; var sdk = await BreezSdkSparkMethods.ConnectWithSigner(new ConnectWithSignerRequest( config: config, breezSigner: signers.breezSigner, sparkSigner: signers.sparkSigner, storageDir: "./.data" )); ``` ### Javascript (Wasm) ```typescript const turnkeyConfig: TurnkeyConfig = { baseUrl: undefined, organizationId: '', apiPublicKey: '', apiPrivateKey: '', walletId: '', network: 'mainnet', accountNumber: undefined, // Set after the first connect to make later signer setup network-free identityPublicKey: undefined, retry: undefined, maxRps: undefined } const signers = await createTurnkeySigner(turnkeyConfig) const config = defaultConfig('mainnet') config.apiKey = '' const sdk = await connectWithSigner( config, signers.breezSigner, signers.sparkSigner, 'breez_spark_db' // For WASM, this is the IndexedDB database name ) ``` ### React Native ```typescript const turnkeyConfig: TurnkeyConfig = { baseUrl: undefined, organizationId: '', apiPublicKey: '', apiPrivateKey: '', walletId: '', network: Network.Mainnet, accountNumber: undefined, // Set after the first connect to make later signer setup network-free identityPublicKey: undefined, retry: undefined, maxRps: undefined, proxy: undefined } const signers = await createTurnkeySigner(turnkeyConfig) const config = defaultConfig(Network.Mainnet) config.apiKey = '' const sdk = await connectWithSigner({ config, breezSigner: signers.breezSigner, sparkSigner: signers.sparkSigner, storageDir: `${RNFS.DocumentDirectoryPath}/data` }) ``` ### Python ```python turnkey_config = TurnkeyConfig( base_url=None, organization_id="", api_public_key="", api_private_key="", wallet_id="", network=Network.MAINNET, account_number=None, # Set after the first connect to make later signer setup network-free identity_public_key=None, retry=None, max_rps=None, ) signers = await create_turnkey_signer(config=turnkey_config) config = default_config(Network.MAINNET) config.api_key = "" sdk = await connect_with_signer(ConnectWithSignerRequest( config=config, breez_signer=signers.breez_signer, spark_signer=signers.spark_signer, storage_dir="./.data" )) ``` ### Go ```go turnkeyConfig := breez_sdk_spark.TurnkeyConfig{ BaseUrl: nil, OrganizationId: "", ApiPublicKey: "", ApiPrivateKey: "", WalletId: "", Network: breez_sdk_spark.NetworkMainnet, AccountNumber: nil, // Set after the first connect to make later signer setup network-free IdentityPublicKey: nil, Retry: nil, MaxRps: nil, } signers, err := breez_sdk_spark.CreateTurnkeySigner(turnkeyConfig) if err != nil { return nil, err } config := breez_sdk_spark.DefaultConfig(breez_sdk_spark.NetworkMainnet) apiKey := "" config.ApiKey = &apiKey sdk, err := breez_sdk_spark.ConnectWithSigner(breez_sdk_spark.ConnectWithSignerRequest{ Config: config, BreezSigner: signers.BreezSigner, SparkSigner: signers.SparkSigner, StorageDir: "./.data", }) if err != nil { return nil, err } ``` A few notes on the configuration: - `api_private_key` is a server secret. It authenticates every Turnkey request; keep it out of client code and logs. - The API key pair can be secp256k1 or P-256 (Turnkey's console default). All published bindings support both. If you use the Rust crate directly, enable the `turnkey` cargo feature, plus `turnkey-p256` for P-256 keys. - `max_rps` paces requests to Turnkey. Unset uses Turnkey's documented limit of 10 requests per second per sub-organization; set it if your account has a different limit. ### Reconnecting without network calls Server deployments often build a fresh SDK instance per request. Setting `identity_public_key` makes the signer setup network-free. Obtain it once from a freshly built signer with `get_identity_public_key` (available right after `create_turnkey_signer`, no connect needed), or from `identity_pubkey` on `get_info` if you already have a connected SDK. Store it alongside the wallet and pass it in the config on later connects. It is a stable, non-secret value, but it must belong to the same wallet. ### Wallets under a deny-export policy `create_turnkey_signer` keeps every Spark key in the enclave, but exports one dedicated non-Spark key on first use for local encryption operations. If your Turnkey policy forbids any key export, use `create_turnkey_signing_only_signer` instead: no key is ever exported. Connect its signers as described in [Signers Without Local ECIES/HMAC Support](/guide/external_signer.md#signers-without-local-ecieshmac-support), which also lists the trade-offs of a signing-only signer. ## Signing models How payments are authorized is decided by the Turnkey policy attached to each credential, not by SDK code. Configure the policies in Turnkey; see the [Turnkey policy documentation](https://docs.turnkey.com/concepts/policies/overview) for the mechanics and [Turnkey's Spark operations](https://docs.turnkey.com/features/networks/spark#supported-operations) for the activity names the policy scopes. ### Server-side signing The server's API credential is allowed to run all Spark signing activities. Every SDK flow then works exactly as documented, starting with [Sending payments](/guide/send_payment.md): the server prepares, signs and sends on its own. Use this when the server is trusted to send payments autonomously. ### User-approved payments The policy allows the server's credential to run everything except the transfer approval activity (`SPARK_PREPARE_TRANSFER`), which requires the end user's own Turnkey credential (for example a passkey registered with Turnkey). The server then drives the send with the [Client signing](/guide/client_signing.md) flow: it prepares the payment and builds the package, the user approves and signs the one item that needs their credential, and the server publishes it. The rest of the signing (`SPARK_SIGN_FROST`) stays with the server under policy, so a payment can never be sent without the user, and no key leaves Turnkey on either side. ## Availability - Turnkey signers are available on all platforms except Flutter, which does not support external signers (see [Using an External Signer](/guide/external_signer.md)). - In the Rust crate the integration is behind the `turnkey` cargo feature. The published bindings ship with it enabled. --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Unilateral exit](/guide/unilateral_exit.md)** # Unilateral exit A unilateral exit moves your Spark balance onto the Bitcoin blockchain without needing the Spark operators to sign the withdrawal for you. It exists as a safety net: if the operators ever stop cooperating with normal [withdrawals](/guide/send_payment.md), you can still recover your funds on-chain. A unilateral exit is a last resort. It is a multi-step, on-chain process that needs your own Bitcoin (in a separate UTXO) to pay mining fees, and it can take several days to finish because of on-chain timelocks. When a normal cooperative withdrawal is available, it is always cheaper and faster: prefer it. ## Before you start Three things are important to know before you build an exit: - **The exit data has to already be on the device.** Quoting and building an exit read each leaf's pre-signed transactions from local storage, so both work with the operators unreachable. What they cannot do is obtain that data: a leaf can be exited this way only once it has been synced at least once while the operators were reachable. The SDK collects it as funds arrive, in the background where background services run and otherwise during `sync_wallet`, which you can turn off with [`exit_chain_auto_fetch_enabled`](/guide/config.md#unilateral-exit-data). Call `sync_wallet` before going offline to run the collection at a moment of your choosing rather than waiting on the background one. Once collected it can be kept outside the SDK's storage, see [Back up the exit data](#back-up-the-exit-data). - **You pay the fees from your own UTXO.** The pre-signed transactions carry no fee, so each is fee-bumped with a child transaction (CPFP) funded by a Bitcoin UTXO you provide. That UTXO must be **native SegWit** (a witness-program script). P2WPKH and P2TR are handled by the built-in signer; any other witness program (for example a P2WSH multisig) works through the `CpfpFundingKind::Custom` funding kind and a custom signer (see [The signer](#the-signer)). Legacy (non-SegWit) scripts are rejected. - **You broadcast the transactions yourself.** The SDK builds and signs the full set but never broadcasts. You send them to the network over time, in order, as their timelocks mature. See [Broadcasting the transactions](#broadcast-the-transactions). ## How it works Your balance is held in a tree of pre-signed Bitcoin transactions. Each leaf is a portion of the balance. To move a leaf on-chain you broadcast the chain of transactions from the tree down to that leaf, then a refund transaction, then a final sweep to your destination address. Because the pre-signed transactions pay no fee on their own, each one is broadcast together with a CPFP child that pays its fee. The exit is three calls: 1. `prepare_unilateral_exit` quotes the exit: it picks which leaves to exit and reports the exact fee and how much to fund, without needing any funding UTXOs yet. 2. `unilateral_exit` takes that quote plus your funding UTXOs and a signer, and returns the complete, signed set of transactions to broadcast. **Store what it returns.** 3. `check_unilateral_exit` takes what you stored and tells you where the exit has got to and what to send next. **Store what it returns, in place of what you had.** An exit runs for days, so you will call the third one many times: after each broadcast, and whenever you want to know how far along it is. ### A single leaf With one leaf there is no fan-out: your funding UTXO pays the fees directly. You broadcast the tree transactions top to bottom, each with its CPFP child as a package, then the refund once its timelock matures, then the sweep. ![Single-leaf unilateral exit](/guide/images/unilateral_exit_single_leaf.svg) The blue transactions come pre-signed and fixed; you cannot change them. The grey CPFP children and the green sweep are built for you from the funding you supply, and are what actually pay the fees and deliver the funds to your address. ### Multiple leaves Exiting several leaves at once starts with a **fan-out** transaction that splits a single funding UTXO into one output per branch. Leaves that share ancestors in the tree share those transactions too, so a shared ancestor is broadcast only once. Every branch's refund is then pulled into a single sweep. ![Multi-leaf unilateral exit](/guide/images/unilateral_exit_multi_leaf.svg) ## Leaf denominations and exit cost Every leaf is exited by its own chain of transactions, so it carries its own on-chain fee whatever its value. The more leaves your balance is spread across, and the smaller they are, the more of it goes to fees on the way out, and the more low-value leaves an `ExitLeafSelection::Auto` exit abandons as uneconomical dust. How the balance is split into leaves is governed by the SDK's leaf optimization, which balances everyday payment experience against unilateral exit value. More, smaller denominations let payments go out without leaf swaps, while fewer, larger denominations cost less to exit. The default leans toward payment experience, which suits most wallets, since a unilateral exit is a rare last resort. See [Custom leaf optimization](/guide/optimize.md) to understand this tradeoff and adjust it if your use case calls for it. ## Quote the exit Call `prepare_unilateral_exit` with the target `fee_rate_sat_per_vbyte`, the `funding_kind` of UTXO you will pay fees with, your `destination` address, and a `selection`. `ExitLeafSelection::Auto` exits every leaf worth more than its own exit cost; `ExitLeafSelection::Specific` exits exactly the leaves you name. The quote returns a `PrepareUnilateralExitResponse`. Its fields tell you how much Bitcoin to gather and how to structure it: - `recoverable_value_sat` is the total value of the selected `leaves`, and `total_fee_sat` is the on-chain fee to recover it, broken down into its three components below. Compare them to decide whether the exit is worth it at the current fee rate. - `single_utxo_funding_sat` is the simplest option: fund **one** UTXO of at least this many satoshis and the SDK fans it out across branches. - `per_branch_funding` lets you skip the fan-out (and its `fanout_fee_sat`) by funding **one UTXO per branch**, each of at least the amount in its `PerBranchFunding` entry. So you do not have to guess how much to send or how many UTXOs to prepare: the quote tells you both. ### The fee components, and what arrives An exit pays its mining fees from two different places, so `total_fee_sat` comes with the split that says which is which. Both `prepare_unilateral_exit` and `unilateral_exit` report all four numbers. | Component | Paid by | |---|---| | `cpfp_fee_sat` | The funding UTXOs, through the CPFP children that fee-bump the tree transactions | | `fanout_fee_sat` | The funding UTXO, by the fan-out transaction. Zero when there is no fan-out | | `sweep_fee_sat` | The value being recovered, by the final sweep | The three always add up to the total: `cpfp_fee_sat` plus `fanout_fee_sat` plus `sweep_fee_sat` is `total_fee_sat`. The first two come out of the Bitcoin you supplied as funding and do not reduce what the exit recovers. The third is different: the sweep spends the refunds and pays out what is left after its own fee, so it comes off the money on its way to your address. **What arrives at `destination`** is therefore `recoverable_value_sat` less `sweep_fee_sat`, plus any funding that was not spent on fees. The sweep also collects the leftover change of the CPFP children it built, so unused funding is delivered to the same address rather than left behind. **What the exit costs in total** is `total_fee_sat`, across the funding UTXO and the recovered value together. Beginning with `recoverable_value_sat` in Spark and a funding UTXO worth F, the destination ends up with those two added together, less `total_fee_sat`. `recoverable_value_sat` less `total_fee_sat` is not the arriving amount. It subtracts the CPFP and fan-out fees a second time, when they were already paid from the funding UTXO. `single_utxo_funding_sat` sits above `cpfp_fee_sat` plus `fanout_fee_sat` on purpose. It carries the sweep fee and a small per-branch allowance as headroom, and both come back to you in the sweep. Preparing also reads the chain, and `exit_chain_state` carries back what it found: which nodes are already on-chain, which refunds landed, and which of those have been swept. Pass the whole `PrepareUnilateralExitResponse` to `unilateral_exit` unchanged, so the build covers only the steps still left. You can read it yourself to show how far an exit has got. Under `ExitLeafSelection::Auto` a leaf is kept when its value exceeds its own exit cost, measured per leaf. That per-leaf measure does not include the shared `fanout_fee_sat`, which the single-UTXO path pays once for the whole exit. So when you fund a multi-leaf exit from a **single** UTXO, the fan-out fee can push the total above what you recover, even though every leaf looked profitable on its own. Two rules keep an exit from ever costing more than it returns: 1. **Before funding, require `recoverable_value_sat` to exceed `total_fee_sat`.** These are the actual totals for the quote, fan-out fee included. If the margin is thin or negative, do not proceed as quoted. 2. **Prefer per-branch funding.** Funding one UTXO per branch (`per_branch_funding`) skips the fan-out entirely, so there is no shared fee. Because `ExitLeafSelection::Auto` already keeps only leaves worth more than their own cost, a per-branch-funded auto exit is always net-positive. If the single-UTXO total is not worth it, either fund per branch, or narrow the set: re-quote with `ExitLeafSelection::Specific` naming only the higher-value leaves (dropping the marginal ones removes their cost and can turn the total positive), or wait for a lower fee rate. If nothing is selected (under `ExitLeafSelection::Auto` no leaf is worth exiting at the given fee rate, or there is nothing to exit) the response comes back empty rather than as an error. Check `leaves` before gathering funding. #### Rust ```rust let quote = sdk .prepare_unilateral_exit(PrepareUnilateralExitRequest { fee_rate_sat_per_vbyte: 2, funding_kind: CpfpFundingKind::P2wpkh, destination: "bc1q...your-destination-address".to_string(), selection: ExitLeafSelection::Auto, }) .await?; println!( "Recovering {} sats for {} sats in fees", quote.recoverable_value_sat, quote.total_fee_sat ); println!("Fund a single UTXO of at least {} sats", quote.single_utxo_funding_sat); ``` #### Swift ```swift let quote = try await sdk.prepareUnilateralExit( request: PrepareUnilateralExitRequest( feeRateSatPerVbyte: 2, fundingKind: .p2wpkh, destination: "bc1q...your-destination-address", selection: .auto ) ) print("Recovering \(quote.recoverableValueSat) sats for \(quote.totalFeeSat) sats in fees") print("Fund a single UTXO of at least \(quote.singleUtxoFundingSat) sats") ``` #### Kotlin ```kotlin val quote = sdk.prepareUnilateralExit( PrepareUnilateralExitRequest( feeRateSatPerVbyte = 2u, fundingKind = CpfpFundingKind.P2wpkh, destination = "bc1q...your-destination-address", selection = ExitLeafSelection.Auto ) ) println("Recovering ${quote.recoverableValueSat} sats for ${quote.totalFeeSat} sats in fees") println("Fund a single UTXO of at least ${quote.singleUtxoFundingSat} sats") ``` #### C# ```csharp var quote = await sdk.PrepareUnilateralExit( request: new PrepareUnilateralExitRequest( feeRateSatPerVbyte: 2, fundingKind: new CpfpFundingKind.P2wpkh(), destination: "bc1q...your-destination-address", selection: new ExitLeafSelection.Auto() ) ); Console.WriteLine($"Recovering {quote.recoverableValueSat} sats for {quote.totalFeeSat} sats in fees"); Console.WriteLine($"Fund a single UTXO of at least {quote.singleUtxoFundingSat} sats"); ``` #### Javascript (Wasm) ```typescript const quote = await sdk.prepareUnilateralExit({ feeRateSatPerVbyte: 2, fundingKind: { type: 'p2wpkh' }, destination: 'bc1q...your-destination-address', selection: { type: 'auto' } }) console.log(`Recovering ${quote.recoverableValueSat} sats for ${quote.totalFeeSat} sats in fees`) console.log(`Fund a single UTXO of at least ${quote.singleUtxoFundingSat} sats`) ``` #### React Native ```typescript const quote = await sdk.prepareUnilateralExit({ feeRateSatPerVbyte: BigInt(2), fundingKind: new CpfpFundingKind.P2wpkh(), destination: 'bc1q...your-destination-address', selection: new ExitLeafSelection.Auto() }) console.log(`Recovering ${quote.recoverableValueSat} sats for ${quote.totalFeeSat} sats in fees`) console.log(`Fund a single UTXO of at least ${quote.singleUtxoFundingSat} sats`) ``` #### Flutter ```dart PrepareUnilateralExitRequest request = PrepareUnilateralExitRequest( feeRateSatPerVbyte: BigInt.from(2), fundingKind: const CpfpFundingKind.p2Wpkh(), destination: "bc1q...your-destination-address", selection: const ExitLeafSelection.auto(), ); PrepareUnilateralExitResponse quote = await sdk.prepareUnilateralExit(request: request); print("Recovering ${quote.recoverableValueSat} sats for ${quote.totalFeeSat} sats in fees"); print("Fund a single UTXO of at least ${quote.singleUtxoFundingSat} sats"); ``` #### Python ```python quote = await sdk.prepare_unilateral_exit( request=PrepareUnilateralExitRequest( fee_rate_sat_per_vbyte=2, funding_kind=CpfpFundingKind.P2WPKH(), destination="bc1q...your-destination-address", selection=ExitLeafSelection.AUTO(), ), ) logging.debug( f"Recovering {quote.recoverable_value_sat} sats " f"for {quote.total_fee_sat} sats in fees" ) logging.debug(f"Fund a single UTXO of at least {quote.single_utxo_funding_sat} sats") ``` #### Go ```go quote, err := sdk.PrepareUnilateralExit(breez_sdk_spark.PrepareUnilateralExitRequest{ FeeRateSatPerVbyte: 2, FundingKind: breez_sdk_spark.CpfpFundingKindP2wpkh{}, Destination: "bc1q...your-destination-address", Selection: breez_sdk_spark.ExitLeafSelectionAuto{}, }) if err != nil { return nil, err } log.Printf("Recovering %d sats for %d sats in fees", quote.RecoverableValueSat, quote.TotalFeeSat) log.Printf("Fund a single UTXO of at least %d sats", quote.SingleUtxoFundingSat) ``` ## Build the exit Gather funding that meets the quote, then call `unilateral_exit` with the quote, your real `CpfpInput` funding UTXOs, and a signer. It returns a `UnilateralExitResponse` with the actual `total_fee_sat` and the full transaction set. If the funding is below what the exit needs it returns `SdkError::InsufficientCpfpFunds`, naming the amount. A UTXO an earlier attempt already spent is not an error: see [Funding a second attempt](#funding-a-second-attempt). A very thin-margin exit can fail even when the funding is sufficient: if the recoverable value net of fees would leave the swept output below the destination address's dust limit, the sweep cannot be built and the exit fails. Exit higher-value leaves with `ExitLeafSelection::Specific`, lower the `fee_rate_sat_per_vbyte`, or wait for a cheaper fee rate. The set it builds depends on what is already on-chain. Because each CPFP child spends the previous one, the exit is one connected chain, so to continue it correctly the SDK reads confirmed on-chain state through its chain service: a step already confirmed comes back as `ExitTransactionStatus::Confirmed` and is not rebuilt. If the chain service cannot resolve a step, the SDK falls back to the status the operators reported: a node the operators already consider on-chain is left as-is rather than fee-bumped (bumping an already-confirmed node would invalidate the rest of the chain), and any node whose state still cannot be determined comes back as `ExitTransactionStatus::Unverified` and is treated as not yet confirmed rather than failing the build. You still get the full set back; broadcasting an already-confirmed transaction is harmless, and re-running once the chain service recovers resolves the status. For a more reliable source you can supply your own chain service (see [Customizing the SDK](/guide/customizing.md#with-chain-service)). ### Rust ```rust let secret_key_bytes: Vec = hex::decode("your-secret-key-hex")?; let signer = signer::single_key_cpfp_signer(secret_key_bytes)?; let response = sdk .unilateral_exit( UnilateralExitRequest { prepared: quote, funding_inputs: vec![CpfpInput::P2wpkh { txid: "your-utxo-txid".to_string(), vout: 0, value: 50_000, pubkey: "your-compressed-pubkey-hex".to_string(), }], }, signer, ) .await?; // Store the whole response: it is the only record of the exit. for tx in &response.transactions { if let Some(blocks) = tx.csv_timelock_blocks { println!("{}: wait {} blocks after its parents confirm", tx.txid, blocks); } } ``` ### Swift ```swift let secretKeyBytes = Data(hexString: "your-secret-key-hex")! let signer = try singleKeyCpfpSigner(secretKeyBytes: secretKeyBytes) let response = try await sdk.unilateralExit( request: UnilateralExitRequest( prepared: quote, fundingInputs: [ .p2wpkh( txid: "your-utxo-txid", vout: 0, value: 50_000, pubkey: "your-compressed-pubkey-hex" ) ] ), signer: signer ) for tx in response.transactions { if let blocks = tx.csvTimelockBlocks { print("\(tx.txid): wait \(blocks) blocks after its parents confirm") } } ``` ### Kotlin ```kotlin try { val secretKeyBytes = "your-secret-key-hex".hexToByteArray() val signer = singleKeyCpfpSigner(secretKeyBytes) val response = sdk.unilateralExit( UnilateralExitRequest( prepared = quote, fundingInputs = listOf( CpfpInput.P2wpkh( txid = "your-utxo-txid", vout = 0u, value = 50_000u, pubkey = "your-compressed-pubkey-hex" ) ) ), signer ) for (tx in response.transactions) { tx.csvTimelockBlocks?.let { blocks -> println("${tx.txid}: wait $blocks blocks after its parents confirm") } } } catch (e: Exception) { // handle error } ``` ### C# ```csharp var secretKeyBytes = Convert.FromHexString("your-secret-key-hex"); var signer = BreezSdkSparkMethods.SingleKeyCpfpSigner(secretKeyBytes); var response = await sdk.UnilateralExit( request: new UnilateralExitRequest( prepared: quote, fundingInputs: new CpfpInput[] { new CpfpInput.P2wpkh( txid: "your-utxo-txid", vout: 0, value: 50_000, pubkey: "your-compressed-pubkey-hex" ) } ), signer: signer ); foreach (var tx in response.transactions) { if (tx.csvTimelockBlocks != null) { Console.WriteLine($"{tx.txid}: wait {tx.csvTimelockBlocks} blocks after its parents confirm"); } } ``` ### Javascript (Wasm) ```typescript const secretKeyBytes = Buffer.from('your-secret-key-hex', 'hex') const signer = singleKeyCpfpSigner(secretKeyBytes) const response = await sdk.unilateralExit( { prepared: quote, fundingInputs: [{ type: 'p2wpkh', txid: 'your-utxo-txid', vout: 0, value: 50_000, pubkey: 'your-compressed-pubkey-hex' }] }, signer ) for (const tx of response.transactions) { if (tx.csvTimelockBlocks != null) { console.log(`${tx.txid}: wait ${tx.csvTimelockBlocks} blocks after its parents confirm`) } } ``` ### React Native ```typescript const secretKeyBytes = Buffer.from('your-secret-key-hex', 'hex') // Buffer.buffer is a shared pool slab; slice to this key's own bytes. const signer = singleKeyCpfpSigner( secretKeyBytes.buffer.slice( secretKeyBytes.byteOffset, secretKeyBytes.byteOffset + secretKeyBytes.byteLength ) ) const response = await sdk.unilateralExit( { prepared: quote, fundingInputs: [ new CpfpInput.P2wpkh({ txid: 'your-utxo-txid', vout: 0, value: BigInt(50_000), pubkey: 'your-compressed-pubkey-hex' }) ] }, signer ) for (const tx of response.transactions) { if (tx.csvTimelockBlocks != null) { console.log(`${tx.txid}: wait ${tx.csvTimelockBlocks} blocks after its parents confirm`) } } ``` ### Flutter ```dart List secretKeyBytes = hex.decode("your-secret-key-hex"); UnilateralExitResponse response = await sdk.unilateralExit( request: UnilateralExitRequest( prepared: quote, fundingInputs: [ CpfpInput.p2Wpkh( txid: "your-utxo-txid", vout: 0, value: BigInt.from(50000), pubkey: "your-compressed-pubkey-hex", ), ], ), signerSecretKey: Uint8List.fromList(secretKeyBytes), ); for (UnilateralExitTransaction tx in response.transactions) { if (tx.csvTimelockBlocks != null) { print("${tx.txid}: wait ${tx.csvTimelockBlocks} blocks after its parents confirm"); } } ``` ### Python ```python secret_key_bytes = bytes.fromhex("your-secret-key-hex") signer = single_key_cpfp_signer(secret_key_bytes=secret_key_bytes) response = await sdk.unilateral_exit( request=UnilateralExitRequest( prepared=quote, funding_inputs=[ CpfpInput.P2WPKH( # type: ignore[list-item] txid="your-utxo-txid", vout=0, value=50_000, pubkey="your-compressed-pubkey-hex", ) ], ), signer=signer, ) for tx in response.transactions: if tx.csv_timelock_blocks is not None: logging.debug( f"{tx.txid}: wait {tx.csv_timelock_blocks} blocks after its parents confirm" ) ``` ### Go ```go secretKeyBytes, err := hex.DecodeString("your-secret-key-hex") if err != nil { return err } signer, err := breez_sdk_spark.SingleKeyCpfpSigner(secretKeyBytes) if err != nil { return err } response, err := sdk.UnilateralExit(breez_sdk_spark.UnilateralExitRequest{ Prepared: quote, FundingInputs: []breez_sdk_spark.CpfpInput{ breez_sdk_spark.CpfpInputP2wpkh{ Txid: "your-utxo-txid", Vout: 0, Value: 50_000, Pubkey: "your-compressed-pubkey-hex", }, }, }, signer) if err != nil { return err } for _, tx := range response.Transactions { if tx.CsvTimelockBlocks != nil { fmt.Printf("%s: wait %d blocks after its parents confirm\n", tx.Txid, *tx.CsvTimelockBlocks) } } ``` ### The signer The CPFP children and the fan-out spend your funding UTXOs, so they have to be signed. The SDK does not hold your funding keys; it hands each unsigned transaction to a signer you provide. The built-in single-key signer covers the common case: it signs P2WPKH and P2TR inputs from one secret key. For `CpfpInput::P2tr` funding, pass the **internal, untweaked (BIP86)** key, not the tweaked on-chain output key: the tweaked key derives a scriptPubKey that does not match the UTXO, so the transaction is rejected at broadcast. For anything else (a multisig, a hardware wallet, or keeping key material out of the SDK entirely) implement the `CpfpSigner` interface and describe the funding with `CpfpFundingKind::Custom` (in the quote) and `CpfpInput::Custom` (in the build). Those carry the funding `script_pubkey_hex` and an upper-bound `signed_input_weight` so the fee stays exact for any witness program. The signer receives a serialized PSBT, signs the inputs that are not already finalized, and returns the serialized signed PSBT: Whichever signer you use, the funding inputs must be **native SegWit** (a witness-program script; P2WPKH or P2TR with the built-in signer, any other witness program with a custom one). The exit refers to each transaction by an id it computes before signing, which only stays stable when the signature lives in the witness (native SegWit) rather than in the input script; legacy scripts are rejected, so your signer only ever has to sign native SegWit inputs. #### Rust ```rust struct MyCpfpSigner; #[async_trait::async_trait] impl signer::CpfpSigner for MyCpfpSigner { async fn sign_psbt(&self, psbt_bytes: Vec) -> Result, SignerError> { let signed_psbt_bytes = sign_psbt_with_your_keys(psbt_bytes)?; Ok(signed_psbt_bytes) } } fn sign_psbt_with_your_keys(psbt_bytes: Vec) -> Result, SignerError> { Ok(psbt_bytes) } ``` #### Swift ```swift class CustomCpfpSigner: CpfpSigner { func signPsbt(psbtBytes: Data) async throws -> Data { return try await signPsbtWithYourKeys(psbtBytes: psbtBytes) } private func signPsbtWithYourKeys(psbtBytes: Data) async throws -> Data { return psbtBytes } } ``` #### Kotlin ```kotlin class MyCpfpSigner : CpfpSigner { override suspend fun signPsbt(psbtBytes: ByteArray): ByteArray { return signPsbtWithYourKeys(psbtBytes) } private fun signPsbtWithYourKeys(psbtBytes: ByteArray): ByteArray { return psbtBytes } } ``` #### C# ```csharp class MyCpfpSigner : CpfpSigner { public async Task SignPsbt(byte[] psbtBytes) { return await SignPsbtWithYourKeys(psbtBytes); } async Task SignPsbtWithYourKeys(byte[] psbtBytes) { return await Task.FromResult(psbtBytes); } } ``` #### Javascript (Wasm) ```typescript class CustomCpfpSigner implements CpfpSigner { async signPsbt (psbtBytes: Uint8Array): Promise { return await signPsbtWithYourKeys(psbtBytes) } } const signPsbtWithYourKeys = async (psbtBytes: Uint8Array): Promise => { return psbtBytes } ``` #### React Native ```typescript class CustomCpfpSigner { signPsbt = async (psbtBytes: ArrayBuffer): Promise => { return await signPsbtWithYourKeys(psbtBytes) } } const signPsbtWithYourKeys = async (psbtBytes: ArrayBuffer): Promise => { return psbtBytes } ``` #### Flutter ```dart Future buildExitWithSigner(BreezSdk sdk, PrepareUnilateralExitResponse quote) async { // Flutter cannot pass a foreign CpfpSigner, so it takes a signPsbt callback. UnilateralExitResponse response = await sdk.unilateralExitWithSigner( request: UnilateralExitRequest( prepared: quote, fundingInputs: [ CpfpInput.p2Wpkh( txid: "your-utxo-txid", vout: 0, value: BigInt.from(50000), pubkey: "your-compressed-pubkey-hex", ), ], ), signPsbt: (Uint8List psbtBytes) async { return signPsbtWithYourKeys(psbtBytes); }, ); for (UnilateralExitTransaction tx in response.transactions) { if (tx.csvTimelockBlocks != null) { print("${tx.txid}: wait ${tx.csvTimelockBlocks} blocks after its parents confirm"); } } } // Receives the serialized PSBT, signs the inputs that are not already // finalized, and returns the serialized signed PSBT. Future signPsbtWithYourKeys(Uint8List psbtBytes) async { return psbtBytes; } ``` #### Python ```python class CustomCpfpSigner(CpfpSigner): async def sign_psbt(self, psbt_bytes: bytes) -> bytes: return sign_psbt_with_your_keys(psbt_bytes) def sign_psbt_with_your_keys(psbt_bytes: bytes) -> bytes: raise NotImplementedError("Sign the PSBT's non-finalized inputs with your keys") ``` #### Go ```go type MyCpfpSigner struct{} func (MyCpfpSigner) SignPsbt(psbtBytes []byte) ([]byte, error) { return signPsbtWithYourKeys(psbtBytes) } func signPsbtWithYourKeys(psbtBytes []byte) ([]byte, error) { return psbtBytes, nil } ``` **Flutter** Flutter cannot pass a foreign CpfpSigner, so it exposes two exit calls. unilateralExit takes the funding secret key bytes and uses the built-in single-key signer. unilateralExitWithSigner takes a signPsbt callback that receives the serialized PSBT, signs the inputs that are not already finalized (any scheme), and returns the serialized signed PSBT. ## Store the response Store the whole `UnilateralExitResponse` as soon as you get it, before you broadcast anything. It is the only record of the exit: the signed transactions, the leaves they recover, and the funding you paid with. Losing it means losing the ability to follow or finish the exit, even though the money is still recoverable. Store the one `check_unilateral_exit` returns in its place each time you call it. Nothing else needs keeping alongside it. ## Broadcast the transactions The SDK does not broadcast anything. `transactions` is the complete, signed set in valid broadcast order, and it is yours to send to the network over time. Broadcast each transaction whose `status` is `ExitTransactionStatus::Ready`, and leave the rest until a later `check_unilateral_exit` reports them ready. Because of the timelocks in the tree, a full exit can span several days. **A step left waiting changes who pays its fee** Each step of the exit becomes valid at a certain block. About 50 blocks later, which is roughly eight hours, a second version of that same step becomes valid too. Spark's watchtowers hold that second version as a safety net for a wallet that goes offline part-way through an exit, and can send it to the network once it unlocks. Both versions move the same money to the same place. What differs is where the mining fee comes from. The version the SDK builds for you is paid for by the funding UTXO you supplied, at the fee rate you asked for. The watchtower's version has its fee built in and takes it from the leaf itself, so that fee comes off the amount arriving at your address instead of out of your funding, at a rate you have no say in. The window runs per step, from the moment that step's timelock matures. An exit whose steps go out as they become `ExitTransactionStatus::Ready` keeps the fee split the quote described; one that sits unbroadcast for a day or more can end up paying for parts of itself out of the money it is recovering. ### Broadcast each package together Most steps come as a pair: a tree transaction and its `cpfp_tx_hex` CPFP child. The tree transaction pays no fee on its own, so a normal single-transaction broadcast rejects it; only the child makes the pair pay enough. Broadcast the two together, as a package, with a node that supports package relay, for example Bitcoin Core: ```text bitcoin-cli submitpackage '["", ""]' ``` The **fan-out** and the **sweep** are the exceptions: each pays its own fee and has no CPFP child (`cpfp_tx_hex` is unset), so you broadcast it **alone**, as an ordinary transaction, anywhere, including a public endpoint such as `POST https://mempool.space/api/tx`. Most public broadcast APIs, including mempool.space, accept only one transaction at a time and cannot submit a package, so they reject the zero-fee tree transactions; use a package-relay-capable node (or service) for the pairs. ### Wait for each step to confirm Within a branch you broadcast one package, wait for it to confirm, then broadcast the next. This is a mempool relay limit, not a Bitcoin consensus rule: nodes relay an unconfirmed parent with at most one unconfirmed child (the "one-parent-one-child", or 1P1C, package), so a second still-unconfirmed package stacked on top would not propagate. Once a package confirms, the next one has a confirmed parent and can go out. (A refund's `csv_timelock_blocks` is a separate wait, and that one is a consensus rule.) ### Order and parallelism Follow `depends_on` to order the set: a transaction can go out as soon as the transactions it lists have confirmed. With a single leaf this is one straight line, top to bottom. With several leaves the branches are largely independent, so to finish faster you can broadcast them in parallel and serialize only where `depends_on` actually links them: 1. **The fan-out first, and alone.** It pays its own fee and has no CPFP child, so it is an ordinary single-transaction broadcast. Wait for it to confirm before any branch package: every branch's first package depends on it. 2. **Then the branch packages, each node transaction with its CPFP child.** A shared ancestor appears once, listed in the `depends_on` of every branch that needs it, so you broadcast it a single time. Within a branch, send one package, wait for it to confirm, then the next (the 1P1C limit above); across branches you can work in parallel. 3. **The sweep last, and alone,** once every refund in its `depends_on` has confirmed. ## The transaction set Each `UnilateralExitTransaction` in `transactions` carries: - `kind`: whether it is the fan-out, a tree node, a refund, or the sweep. - `node_id`: the tree node a transaction belongs to (the leaf id for a refund), unset for the fan-out and the sweep. - `txid` and `tx_hex`: the signed transaction to broadcast. - `cpfp_tx_hex`: its signed CPFP child, to broadcast alongside `tx_hex` as a package. Unset for the fan-out and the sweep, and for a step that is already confirmed. - `csv_timelock_blocks`: the relative timelock, in blocks, that must mature before the transaction can confirm. - `depends_on`: the txids of other transactions in the set that must confirm first. - `status`: where the transaction stands. `ExitTransactionStatus::Confirmed` means it is done and can be skipped, and carries the `block_height` it landed at, which is what a `csv_timelock_blocks` on its child counts from. `ExitTransactionStatus::Ready` means broadcast it now. `ExitTransactionStatus::WaitingForDependencies` means something in `depends_on` has yet to confirm. `ExitTransactionStatus::WaitingForTimelock` means its inputs are confirmed but its `csv_timelock_blocks` has not matured, and reports the `spendable_at_height` block it can first be mined in. `ExitTransactionStatus::Unverified` means the chain could not be read for it while the exit was built, so the SDK cannot say whether broadcasting it is safe. Leave it, and build the exit again once the chain service is healthy (see the troubleshooting table). The SDK reads the chain tip to tell these apart, so you do not have to. ## Follow the exit An exit takes days, and you are broadcasting it yourself, so you need a way to ask how far it has got. That is `check_unilateral_exit`: hand it the response you stored, and it hands the same exit back with every transaction's status brought up to date. Store that one in place of what you had. Call it after each broadcast, and whenever you want to show progress. It reads the chain and nothing else: no wallet, no leaves, no signer, no funding. An exit can be followed on a device that has lost everything but the stored response. Its `verdict` says what to do next: - `UnilateralExitVerdict::Valid`: the exit is on track. Broadcast the transactions whose `status` is `ExitTransactionStatus::Ready`. Sending one you already sent is harmless, so you never have to remember what you broadcast. - `UnilateralExitVerdict::Done`: every transaction has confirmed, the sweep included. The money is at your destination address and there is nothing left to do. - `UnilateralExitVerdict::Redo`: this exit cannot finish as it stands. See [Starting over](#starting-over). `UnilateralExitVerdict::Redo` means something on-chain no longer matches the transactions you hold: a different refund for the same leaf confirmed, someone fee-bumped a step in a way yours cannot follow, or funding you were counting on went elsewhere. Your money is not lost, and the fix is always the same: quote and build again. ### Rust ```rust let checked = sdk .check_unilateral_exit(CheckUnilateralExitRequest { exit: stored }) .await?; // Store this one in place of the one you had. let exit = checked.exit; match checked.verdict { UnilateralExitVerdict::Valid => { for tx in &exit.transactions { if matches!(tx.status, ExitTransactionStatus::Ready) { println!("ready to broadcast: {}", tx.txid); } } } UnilateralExitVerdict::Done => { println!("The exit finished: {} sats recovered", exit.recoverable_value_sat); } UnilateralExitVerdict::Redo { reason } => { // Quote and build again, naming the same leaves. Pass exit.funding_inputs // back and the SDK follows them to whatever they have become. println!("Build the exit again: {reason:?}"); } } ``` ### Swift ```swift let checked = try await sdk.checkUnilateralExit( request: CheckUnilateralExitRequest(exit: stored) ) // Store this one in place of the one you had. let exit = checked.exit switch checked.verdict { case .valid: for tx in exit.transactions { if case .ready = tx.status { print("ready to broadcast: \(tx.txid)") } } case .done: print("The exit finished: \(exit.recoverableValueSat) sats recovered") case .redo(let reason): // Quote and build again, naming the same leaves. Pass exit.fundingInputs // back and the SDK follows them to whatever they have become. print("Build the exit again: \(reason)") } ``` ### Kotlin ```kotlin val checked = sdk.checkUnilateralExit( CheckUnilateralExitRequest(exit = stored) ) // Store this one in place of the one you had. val exit = checked.exit when (val verdict = checked.verdict) { is UnilateralExitVerdict.Valid -> { for (tx in exit.transactions) { if (tx.status is ExitTransactionStatus.Ready) { // Log.v("Breez", "ready to broadcast: ${tx.txid}") } } } is UnilateralExitVerdict.Done -> { // Log.v("Breez", "The exit finished: ${exit.recoverableValueSat} sats recovered") } is UnilateralExitVerdict.Redo -> { // Quote and build again, naming the same leaves. Pass exit.fundingInputs // back and the SDK follows them to whatever they have become. // Log.v("Breez", "Build the exit again: ${verdict.reason}") } } ``` ### C# ```csharp var checkedExit = await sdk.CheckUnilateralExit( request: new CheckUnilateralExitRequest(exit: stored) ); // Store this one in place of the one you had. var exit = checkedExit.exit; switch (checkedExit.verdict) { case UnilateralExitVerdict.Valid: foreach (var tx in exit.transactions) { if (tx.status is ExitTransactionStatus.Ready) { Console.WriteLine($"ready to broadcast: {tx.txid}"); } } break; case UnilateralExitVerdict.Done: Console.WriteLine( $"The exit finished: {exit.recoverableValueSat} sats recovered"); break; case UnilateralExitVerdict.Redo { reason: var reason }: // Quote and build again, naming the same leaves. Pass exit.fundingInputs // back and the SDK follows them to whatever they have become. Console.WriteLine($"Build the exit again: {reason}"); break; } ``` ### Javascript (Wasm) ```typescript const checked = await sdk.checkUnilateralExit({ exit: stored }) // Store this one in place of the one you had. const exit = checked.exit switch (checked.verdict.type) { case 'valid': { for (const tx of exit.transactions) { if (tx.status.type === 'ready') { console.log(`ready to broadcast: ${tx.txid}`) } } break } case 'done': { console.log(`The exit finished: ${exit.recoverableValueSat} sats recovered`) break } case 'redo': { // Quote and build again, naming the same leaves. Pass exit.fundingInputs // back and the SDK follows them to whatever they have become. console.log(`Build the exit again: ${checked.verdict.reason}`) break } } ``` ### React Native ```typescript const checked = await sdk.checkUnilateralExit({ exit: stored }) // Store this one in place of the one you had. const exit = checked.exit switch (checked.verdict.tag) { case UnilateralExitVerdict_Tags.Valid: for (const tx of exit.transactions) { if (tx.status.tag === ExitTransactionStatus_Tags.Ready) { console.log(`ready to broadcast: ${tx.txid}`) } } break case UnilateralExitVerdict_Tags.Done: console.log(`The exit finished: ${exit.recoverableValueSat} sats recovered`) break case UnilateralExitVerdict_Tags.Redo: { // Quote and build again, naming the same leaves. Pass exit.fundingInputs // back and the SDK follows them to whatever they have become. const { reason } = checked.verdict.inner console.log(`Build the exit again: ${reason}`) break } } ``` ### Flutter ```dart CheckUnilateralExitResponse checked = await sdk.checkUnilateralExit( request: CheckUnilateralExitRequest(exit: stored), ); // Store this one in place of the one you had. UnilateralExitResponse exit = checked.exit; UnilateralExitVerdict verdict = checked.verdict; if (verdict is UnilateralExitVerdict_Valid) { for (UnilateralExitTransaction tx in exit.transactions) { if (tx.status is ExitTransactionStatus_Ready) { print("ready to broadcast: ${tx.txid}"); } } } else if (verdict is UnilateralExitVerdict_Done) { print("The exit finished: ${exit.recoverableValueSat} sats recovered"); } else if (verdict is UnilateralExitVerdict_Redo) { // Quote and build again, naming the same leaves. Pass exit.fundingInputs // back and the SDK follows them to whatever they have become. print("Build the exit again: ${verdict.reason}"); } ``` ### Python ```python checked = await sdk.check_unilateral_exit( request=CheckUnilateralExitRequest(exit=stored) ) # Store this one in place of the one you had. exit = checked.exit if isinstance(checked.verdict, UnilateralExitVerdict.VALID): for tx in exit.transactions: if isinstance(tx.status, ExitTransactionStatus.READY): logging.debug(f"ready to broadcast: {tx.txid}") elif isinstance(checked.verdict, UnilateralExitVerdict.DONE): logging.debug(f"The exit finished: {exit.recoverable_value_sat} sats recovered") elif isinstance(checked.verdict, UnilateralExitVerdict.REDO): # Quote and build again, naming the same leaves. Pass exit.funding_inputs # back and the SDK follows them to whatever they have become. logging.debug(f"Build the exit again: {checked.verdict.reason}") ``` ### Go ```go checked, err := sdk.CheckUnilateralExit(breez_sdk_spark.CheckUnilateralExitRequest{ Exit: stored, }) if err != nil { return err } // Store this one in place of the one you had. exit := checked.Exit switch verdict := checked.Verdict.(type) { case breez_sdk_spark.UnilateralExitVerdictValid: for _, tx := range exit.Transactions { if _, ready := tx.Status.(breez_sdk_spark.ExitTransactionStatusReady); ready { log.Printf("ready to broadcast: %s", tx.Txid) } } case breez_sdk_spark.UnilateralExitVerdictDone: log.Printf("The exit finished: %d sats recovered", exit.RecoverableValueSat) case breez_sdk_spark.UnilateralExitVerdictRedo: // Quote and build again, naming the same leaves. Pass exit.FundingInputs // back and the SDK follows them to whatever they have become. log.Printf("Build the exit again: %v", verdict.Reason) } ``` ## Funding a second attempt Whenever you call `unilateral_exit` again, you have to give it funding. Two options, and the first is simpler: - **Fresh UTXOs.** Fund the amount the new quote asks for and pass those. Nothing to keep track of. - **The same UTXOs as last time.** Pass back the `funding_inputs` the stored response carries. An earlier attempt will have spent them, and that is fine: the SDK follows each outpoint to what your money became, whether that is a fan-out output, the change of a fee-bumping transaction, or several steps of both. Only what came from money you supplied, and still pays a script you control, is used. Either way you can add more: pass the old funding *and* a fresh UTXO when the exit needs more than what is left. The exit is short only when what you gave it, once followed, cannot cover what remains. Then it returns `SdkError::InsufficientCpfpFunds` with the amount it needs. ## Starting over Three things send you back to `prepare_unilateral_exit` and `unilateral_exit`. In every case you build the exit again from scratch: you never hand a previously built transaction back to the SDK. **You want to pay a higher fee rate.** On-chain fees rise, and an exit already under way stops confirming. Quote again at the higher `fee_rate_sat_per_vbyte`, naming the same leaves with `ExitLeafSelection::Specific`, and build again. Whatever has already confirmed stays as it is and costs nothing to keep; only what has not yet confirmed is rebuilt at the higher rate, and it replaces the earlier version on the network (RBF). The fee you are quoted is for the part that is left, so it is less than a fresh exit of the same leaves. **`check_unilateral_exit` returned `UnilateralExitVerdict::Redo`.** The chain no longer matches the exit you hold, so those transactions cannot finish. Quote and build again the same way. Your funds are not lost: they are still in the tree, or already in an output you control, and the new exit picks up from wherever they are. **A transaction came back `ExitTransactionStatus::Unverified`.** The chain could not be read for it while the exit was built, so the SDK cannot tell whether an earlier fee-bumping child already spent the funding this one would use. `check_unilateral_exit` cannot settle that: it reads only the exit you kept, never your funding. Building again does, because it follows your funding to what it is worth now, so quote and build again once the chain service is healthy. Name the leaves with `ExitLeafSelection::Specific` rather than `ExitLeafSelection::Auto` both times, taking the ids from your stored response. This is the dependable way to pick an exit back up, including a leaf still waiting out its refund timelock. Both calls read the chain, so both price only what is left. A leaf far enough along stays worth exiting under `ExitLeafSelection::Auto` even when a fresh exit of it would not be. ## Back up the exit data The transactions an exit is built from are held in the SDK's local storage. While the operators are reachable they can be fetched again, so a wallet restored from its seed rebuilds them on its own. When that storage is gone and the operators are unreachable, they cannot be recovered from anywhere, and the leaves they cover cannot be exited. `export_unilateral_exit_state` returns that data as a single opaque value, covering every leaf the wallet holds together with the transactions that spend it. It reflects what is present when it is called: a leaf whose data has not been collected yet is exported without it. The value grows with the number of leaves and can reach several megabytes. Treat the value as sensitive. Carrying every leaf and its transactions, it discloses the wallet's balance, how that balance is split up, and the history of what the wallet has received and spent. Encrypt it wherever you keep it. ### Rust ```rust let exported = sdk.export_unilateral_exit_state().await?; // Keep the state somewhere the wallet's own storage cannot take with it. println!("Exit state is {} bytes", exported.exit_state.len()); ``` ### Swift ```swift let exported = try await sdk.exportUnilateralExitState() // Keep the state somewhere the wallet's own storage cannot take with it. print("Exit state is \(exported.exitState.count) bytes") ``` ### Kotlin ```kotlin val exported = sdk.exportUnilateralExitState() // Keep the state somewhere the wallet's own storage cannot take with it. // Log.v("Breez", "Exit state is ${exported.exitState.length} bytes") ``` ### C# ```csharp var exported = await sdk.ExportUnilateralExitState(); // Keep the state somewhere the wallet's own storage cannot take with it. Console.WriteLine($"Exit state is {exported.exitState.Length} bytes"); ``` ### Javascript (Wasm) ```typescript const exported = await sdk.exportUnilateralExitState() // Keep the state somewhere the wallet's own storage cannot take with it. console.log(`Exit state is ${exported.exitState.length} bytes`) ``` ### React Native ```typescript const exported = await sdk.exportUnilateralExitState() // Keep the state somewhere the wallet's own storage cannot take with it. console.log(`Exit state is ${exported.exitState.length} bytes`) ``` ### Flutter ```dart ExportUnilateralExitStateResponse exported = await sdk.exportUnilateralExitState(); // Keep the state somewhere the wallet's own storage cannot take with it. print("Exit state is ${exported.exitState.length} bytes"); ``` ### Python ```python exported = await sdk.export_unilateral_exit_state() # Keep the state somewhere the wallet's own storage cannot take with it. logging.debug(f"Exit state is {len(exported.exit_state)} bytes") ``` ### Go ```go exported, err := sdk.ExportUnilateralExitState() if err != nil { return "", err } // Keep the state somewhere the wallet's own storage cannot take with it. log.Printf("Exit state is %v bytes", len(exported.ExitState)) ``` The SDK emits `SdkEvent::UnilateralExitStateChanged` once it has completed the data for a leaf that was missing it, and whenever it rebuilds a leaf's data. That is the point at which a previously exported value stops covering the wallet. A leaf the operators answer for only in part is not announced: what came back still cannot back an exit, and it stays that way until they complete it. `import_unilateral_exit_state` puts an exported value back. It does not contact the operators, so it works while they are unreachable, and the value must come from the same network the SDK is configured for. A leaf is taken only when the exit state records this wallet as its owner; the rest are skipped and counted in `skipped_foreign_leaves`. For the leaves it does take, the wallet keeps whatever exit data it can already exit with. An exported value carries no mark of when it was taken, so nothing in it says it is newer than what is on the device; the imported copy is used only for a leaf the wallet has nothing usable for, and only when that copy is complete on its own. Importing an out of date or half-collected value therefore never leaves a leaf less exitable than it already was. Leaves the wallet keeps but whose imported copy it did not use are counted in `skipped_chains`. A leaf is dropped outright when its imported copy disagrees with a node the wallet already holds, on a value that cannot change over a node's lifetime. One of the two copies is then simply wrong about that node, and nothing in the entry is trusted on the strength of it, so the leaf is not restored at all. These are counted separately, in `skipped_conflicting_leaves`, because unlike the counts above they mark exit data the import could not put back. ### Rust ```rust let imported = sdk .import_unilateral_exit_state(ImportUnilateralExitStateRequest { exit_state }) .await?; println!( "Imported {} leaves, skipped {}", imported.imported_leaves, imported.skipped_foreign_leaves ); ``` ### Swift ```swift let imported = try await sdk.importUnilateralExitState( request: ImportUnilateralExitStateRequest(exitState: exitState) ) print("Imported \(imported.importedLeaves) leaves, skipped \(imported.skippedForeignLeaves)") ``` ### Kotlin ```kotlin val imported = sdk.importUnilateralExitState( ImportUnilateralExitStateRequest(exitState) ) // Log.v( // "Breez", // "Imported ${imported.importedLeaves} leaves, skipped ${imported.skippedForeignLeaves}" // ) ``` ### C# ```csharp var imported = await sdk.ImportUnilateralExitState( request: new ImportUnilateralExitStateRequest(exitState: exitState) ); Console.WriteLine($"Imported {imported.importedLeaves} leaves, " + $"skipped {imported.skippedForeignLeaves}"); ``` ### Javascript (Wasm) ```typescript const imported = await sdk.importUnilateralExitState({ exitState }) console.log(`Imported ${imported.importedLeaves} leaves, skipped ${imported.skippedForeignLeaves}`) ``` ### React Native ```typescript const imported = await sdk.importUnilateralExitState({ exitState }) console.log(`Imported ${imported.importedLeaves} leaves, skipped ${imported.skippedForeignLeaves}`) ``` ### Flutter ```dart ImportUnilateralExitStateResponse imported = await sdk.importUnilateralExitState( request: ImportUnilateralExitStateRequest(exitState: exitState), ); print("Imported ${imported.importedLeaves} leaves, skipped ${imported.skippedForeignLeaves}"); ``` ### Python ```python imported = await sdk.import_unilateral_exit_state( request=ImportUnilateralExitStateRequest(exit_state=exit_state) ) logging.debug( f"Imported {imported.imported_leaves} leaves, " f"skipped {imported.skipped_foreign_leaves}" ) ``` ### Go ```go imported, err := sdk.ImportUnilateralExitState(breez_sdk_spark.ImportUnilateralExitStateRequest{ ExitState: exitState, }) if err != nil { return err } log.Printf("Imported %d leaves, skipped %d", imported.ImportedLeaves, imported.SkippedForeignLeaves) ``` An out of date value can restore leaves that have since been spent, so the balance may read high until the next sync reconciles it with the operators. ## Troubleshooting | Problem | Cause | Solution | |---------|-------|----------| | `prepare_unilateral_exit` returns no `leaves` | Under `ExitLeafSelection::Auto`, no leaf is worth exiting at the current rate | Lower `fee_rate_sat_per_vbyte` or wait for cheaper on-chain fees (this is not an error) | | A leaf you are mid-exit on is missing from a new `ExitLeafSelection::Auto` quote | The new quote reselected leaves instead of naming them | Quote with `ExitLeafSelection::Specific`, naming the leaves from your stored response | | `check_unilateral_exit` returns `UnilateralExitVerdict::Redo` | Something on-chain no longer matches the transactions you hold | Quote and build again, naming the same leaves; see [Starting over](#starting-over) | | The exit has stopped confirming | On-chain fees rose above what its transactions pay | Quote and build again at a higher `fee_rate_sat_per_vbyte`; see [Starting over](#starting-over) | | Less arrived than `recoverable_value_sat` less `sweep_fee_sat` | A step sat unbroadcast long enough for a watchtower to send its own version, which pays its fee out of the leaf | Broadcast each step while it is `ExitTransactionStatus::Ready`; see [A step left waiting changes who pays its fee](#broadcast-the-transactions) | | `total_fee_sat` is close to or above `recoverable_value_sat` | The shared fan-out fee makes a single-UTXO multi-leaf exit uneconomical | Fund one UTXO per branch (`per_branch_funding`) to drop the fan-out fee, exit fewer leaves with `ExitLeafSelection::Specific`, or wait for a lower fee rate | | The build/sweep fails with a "below the dust limit" error | The recoverable value net of fees is below the destination's dust limit | Exit higher-value leaves with `ExitLeafSelection::Specific`, lower the `fee_rate_sat_per_vbyte`, or wait for a cheaper fee rate | | `SdkError::InsufficientCpfpFunds` | The funding you gave, once followed to what it became, is below what the exit needs | Fund at least `single_utxo_funding_sat`, or the amount in each `PerBranchFunding`; you can pass fresh UTXOs alongside the old ones | | "min relay fee not met" when broadcasting | The package fee is too low for the network | Increase `fee_rate_sat_per_vbyte`, rebuild, and re-broadcast (RBF) | | "mandatory-script-verify-flag-failed" | A CPFP child was not signed correctly | Ensure your `CpfpSigner` signs every non-finalized input | | "non-BIP68-final" | A relative timelock has not matured | Wait until `status` leaves `ExitTransactionStatus::WaitingForTimelock` | | A tree transaction is rejected on its own | The zero-fee parent was broadcast without its child | Broadcast the parent and its `cpfp_tx_hex` together as a package | | The sweep is rejected | Not every refund it spends has confirmed | Wait until `check_unilateral_exit` reports it `ExitTransactionStatus::Ready` | | A transaction's `status` is `ExitTransactionStatus::Unverified` | The chain service was unavailable or rate-limited, so the SDK could not tell whether that step is already on-chain, nor whether an earlier fee-bumping child already spent the funding it would use | Do not broadcast it. Quote and build the exit again once the chain service is healthy; see [Starting over](#starting-over), and [Customizing the SDK](/guide/customizing.md#with-chain-service) for a more reliable service | --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Moving to production](/guide/moving_to_production.md)** # Moving to production ## Production checklist Before moving to production, we strongly recommend verifying that these use cases are correctly implemented. Here is a checklist you can use to ensure that your application is production ready. - **Add logging**: Add sufficient logging into your application to diagnose any issues users are having. Include log entries from the Breez SDK up to and including **DEBUG** level. For more information see [Adding logging](/guide/logging.md). > ⚠️ Proper logging is a prerequisite for troubleshooting. If logging is not implemented (or is implemented incorrectly), the Breez team will not be able to assist in diagnosing or resolving reported issues. - **Display pending payments**: Payments always contain a status field that can be used to determine whether the payment was completed or not. Make sure you handle the case where the payment is still pending by showing the correct status to the user. - **Claiming on-chain deposits**: Make sure you handle the case where an on-chain deposit is unclaimed. For more information see [Claiming on-chain deposits](/guide/onchain_claims.md). - **Check Spark status**: Use the Spark status check to monitor the operational status of the Spark network and display a warning to users when services are degraded or experiencing issues. For more information see [Spark status](/guide/spark_status.md). - **Server-side deployments**: If you're running the SDK in a multi-tenant server hosting many wallets at once, review [Server mode](/guide/server_mode.md) for the recommended profile, explicit sync/claim patterns, and shared-infrastructure wiring. **→ [Overview](/guide/uxguide.md)** # UX Guidelines These guidelines describe how to build a UI/UX on top of the Breez SDK that feels natural to end users. They are design principles, not feature documentation: each page says what experience to create and why, and links to the feature guides for the how. > **Reference:** These guidelines are implemented in **[Glow](https://glow-app.co)**. Use it as the primary UX reference during SDK implementation, and adapt the recommendations to your own use case. ## Core UX principles - **Simplicity over choice**: users should not have to pick protocols or rails unless absolutely necessary. The wallet knows what a pasted string is and what to do with it. - **Transparency without jargon**: show limits, fees, and conditions up front in plain language, at the moment they matter. - **Progressive disclosure**: keep advanced details available but tucked away by default. - **Hide the rails**: payments ride on Lightning, Spark, and conversions under the hood. Users think in terms of people, dollars, and bitcoin, never in terms of plumbing. ## Guidelines - **[Login & backup]** - **[Displaying payments]** - **[Receiving payments]** - **[Sending payments]** [Receiving payments]: uxguide_receive.md [Sending payments]: uxguide_send.md [Displaying payments]: uxguide_display.md [Login & backup]: uxguide_login.md **→ [Login & backup](/guide/uxguide_login.md)** ## Login & backup How users get into a wallet and how they recover it are one story: the login method determines the backup story. These guidelines cover both. ### UX principles - **Onboarding should be seamless.** Don't block first use with backup ceremonies. - **Passkey-first where supported.** Passkeys remove the mnemonic from onboarding entirely while preserving self-custody. Be transparent about the trust model: the passkey is the wallet, not a convenience layer on top of it. - **The mnemonic is the universal fallback.** It remains the onboarding path on devices without passkey support, and the manual backup path for everyone else. - **OS prompts are precious.** Every biometric prompt costs trust and attention. Design flows around the minimum number of prompts, and treat a dismissed prompt as a user decision, not an error to retry. ### Onboarding 1. **Only offer what the device can deliver.** Check passkey availability at startup (`PasskeyClient.check_availability`) and fall back to mnemonic onboarding when the device or configuration can't support passkeys. One check covers both. 2. **One button when the platform allows it.** On iOS and Android, a single primary action (`PasskeyClient.connect_with_passkey`) serves both new and returning users: it signs a returning user in silently and falls through to registration for a new one. On web, do the same where `PasskeyClient.supports_immediate_mediation` is true; otherwise split into "Create a new passkey" and "Sign in with a passkey", because without immediate mediation WebAuthn can't distinguish "no credential" from "user cancelled". 3. **Keep the mnemonic path reachable but quiet**: a small "Use recovery phrase instead" link under the primary action, not a competing button. 4. **Say what the passkey is before creating it.** A short warning that the passkey is how the user accesses their funds, and that deleting it may make funds permanently inaccessible. Beyond that, add no consent screen of your own: the OS already shows one. 5. **Narrate the wait.** Passkey setup has several distinct phases; label each one specifically ("Verifying app domain...", "Detecting passkey...", "Initializing...") rather than showing one generic spinner. 6. **For mnemonic onboarding**, present the generated phrase as a numbered word grid with copy support, and require an explicit "I've saved my phrase" confirmation before moving on. ### Returning users 1. **Day-2 login is effortless.** Don't run a full passkey ceremony on every launch: on native, store the seed in the keychain so the wallet opens directly (optionally biometric-bound, making the return a single biometric prompt); on web, offer "Sign in with a passkey". Always leave an escape hatch ("Use a different wallet"). 2. **The mnemonic never touches plain storage.** On native, keep it in the keychain or secure storage; on web, re-derive it from the passkey each session. Within a session, cache the derived seed in memory so later SDK calls don't re-prompt. 3. **A dismissed prompt is an answer.** Never auto-retry: land on a persistent error state with a "Try Again" button, and re-prompt only on an explicit tap. (The SDK follows the same rule and never re-fires the OS prompt on its own.) 4. **Catch duplicate registrations.** When the authenticator reports that a passkey already exists for this wallet, say so and pivot the user to sign-in instead of silently prompting again. ### Backup 1. **Backup follows value, not signup.** Invite the user to back up after the wallet is created, or after the first payment arrives, never as a gate in front of first use. 2. **Explain, then verify.** Say plainly why the phrase must be written down and stored safely, and consider confirming the backup with a partial re-entry. 3. **Passkey users still get a recovery phrase.** Offer a user-initiated "Show recovery phrase" that derives the mnemonic on demand (`PasskeyClient.sign_in`), so losing the passkey is survivable. Require the re-authentication to use the same credential that owns the wallet, so a sibling credential can't reveal a different wallet's phrase. 4. **Protect phrase screens from capture.** Enable screen-capture protection wherever a mnemonic is displayed or entered. ### Platform notes Browsers and native authenticators expose different error semantics, so the recommended flows differ by platform. **iOS 18+ / Android 9+:** one "Use Passkey" button. A returning user gets a single biometric prompt; a new user fast-fails silently and falls through to registration. On a real cancel, show the persistent retry state and do not auto-register. | Path | OS prompts | |---|---| | Returning user | **1** (one assertion derives master + label) | | New user | **1** (the create ceremony derives master + label) | A new user costs **2** (1 create, 1 assertion) on authenticators that report PRF support without evaluating it during creation, where the SDK falls back to a follow-up assertion. **Web:** where the browser supports immediate mediation (`PasskeyClient.supports_immediate_mediation`), a single "Use Passkey" button; otherwise "Create a new passkey" and "Sign in with a passkey" as separate actions. `PasskeyClient.connect_with_passkey` is not surfaced on the WASM target. **Multiple wallets per passkey:** when a user adds a wallet under a new label, sign in with the new label first and store the label second; that order costs one OS prompt instead of two. See [Managing labels](/guide/passkey_labels.md). ### Continuity & recovery - **Keep returning users on the same wallet.** Persist the credential metadata returned by each flow so the app recognizes the user's passkey, prevents duplicate registrations on the same device, and can show which authenticator holds the passkey and whether it syncs. Treat authenticator identity and backup flags as display hints, never as trust signals. See [Credential metadata](/guide/passkey_credential_metadata.md). - **Every failure has a next step.** Passkey errors normalize to `PrfProviderError` variants, each mapping to a recovery action; see the [error-recovery table](/guide/passkey_onboarding.md#error-recovery). On iOS, the SDK disambiguates the platform's generic failure (missing credential, cancel, or timeout) for you. --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere. **→ [Displaying payments](/guide/uxguide_display.md)** ## Displaying payments ### UX principles - History should be **clear, transparent, and verifiable**. - Offer both **simple summaries** and **deeper technical details**: a glanceable list, with proof underneath for whoever needs it. ### Guidelines 1. **Display fees separately** from the amount, never silently folded in. 2. **Name payments after people.** Prefer the contact name, then the Lightning address, over the invoice description in payment titles. Resolve contact names when rendering, so renaming a contact updates their whole history; keep the raw address available in the details view. 3. **Keep the proof one tap away.** Show the payment's metadata, at minimum the invoice and preimage, under an expandable details section so the list stays uncluttered but every payment remains verifiable. 4. **Make state unmistakable**: pending, succeeded, and failed payments each get distinct visuals. A payment whose conversion or cross-chain delivery is still in flight is pending, even though the user's part is done. 5. **Show conversions honestly.** When the user converts between bitcoin and USD, that's an event they initiated, so it appears in the history (e.g. "Conversion to USD"). When a conversion is merely the internal step of a payment in progress, it doesn't: the user sees one payment, not its plumbing. Denominate stablecoin-related payments in USD, and put the conversion breakdown (provider, amounts, fee) under the details section. 6. **Lead with what the recipient got.** For USDC/USDT sends, the details view opens with the delivered amount, network, recipient address, and provider, ahead of the internal transfer metadata. **→ [Receiving payments](/guide/uxguide_receive.md)** ## Receiving payments ### UX principles - Receiving should feel like **sharing an identifier**, akin to sharing an email address, not like executing a multi-step process. - **Lightning first**: other rails are fallbacks the wallet falls back to, not choices the user must make. ### Guidelines 1. **Make Lightning the primary way to receive.** Lightning is the common language of Bitcoin. Treat on-chain Bitcoin as a secondary on-ramp, offered only if your use case needs it. 2. **Don't expose implementation addresses** (i.e. Spark) unless absolutely necessary. Every extra option adds confusion, and until Spark supports dynamic addresses, exposing a Spark address carries privacy trade-offs. 3. **Show a reusable QR code by default** (LNURL-Pay, the most widely supported reusable method), with a fallback to a BOLT11 invoice for one-off requests with a specific amount. 4. **Give every user a human-readable Lightning address.** Register a random one automatically so receiving works from the first moment, and let the user customize it later. If they change it, tell them the old address stops receiving payments. It stays reserved for them, so no one else can take it and they can switch back later. 5. **Expose two primary actions**: **Copy** (the Lightning address) and **Share** (the LNURL-Pay string). This matches the patterns of popular Lightning wallets and maximizes compatibility. 6. **Respect the active denomination.** When [Stable Balance](/guide/uxguide_send.md#stable-balance) is active, let users request amounts in USD and announce received payments in USD. The conversion from incoming bitcoin happens automatically; the user just sees dollars arrive. 7. **Show limits and fees before they bite.** If a payment request carries a receive fee or amount limits, display them when the request is created, not after the payment fails. 8. **Reflect payment progress as it happens**, driven by SDK events: see the [receive payment UX recommendations](/guide/receive_payment.md#lightning-1). ### On-chain deposits The SDK claims incoming on-chain deposits automatically. The UX only needs to surface the exceptions: a deposit still confirming, a claim that costs more than the configured maximum, and a refund the user chose. See [Claiming on-chain deposits](/guide/onchain_claims.md). 1. **Deposits are payments.** Show incoming deposits in the payment history as pending entries while they confirm and while claiming runs. Don't invent a separate deposits screen. 2. **Silence is the happy path.** When a deposit confirms and the claim fee is within bounds, the SDK claims it with no user action; the user simply sees the payment complete. 3. **Ask when it costs more than expected.** When claiming would exceed the configured maximum fee, present a clear choice: approve, showing the amount, the network fee, and what the user will actually receive, or reject. 4. **A rejected deposit stays visible until resolved.** Keep a persistent indicator that leads to a refund flow: destination address, a fee-speed choice, and the refund transaction id once broadcast. **→ [Sending payments](/guide/uxguide_send.md)** ## Sending payments ### UX principles - Provide a **unified entry point** that "just works" regardless of what the user pastes or scans. - Users should recognize **people and amounts**, not standards (BOLT11 vs. LNURL vs. address). - **Denominate in what the user holds**: when Stable Balance is active, amounts are entered in USD. ### Guidelines 1. **One send flow for everything.** A single input accepts BOLT11, Lightning address, Bitcoin address, LNURL, USDC/USDT address, or a contact. Parsing decides what happens next; the user never picks a payment type. 2. Add **on-chain Bitcoin** to that same flow as an off-ramp, only if your use case needs it. 3. **Meet the input where it is**: support Paste, Scan (camera QR), and Upload (QR from photos or screenshots). 4. **Offer "Use all funds"** when paying to a Lightning or Bitcoin address. 5. **Validate early, disclose before commitment.** Check amounts against limits and balance as the user types, and show all fees on the confirmation screen, before anything is sent. 6. **Reflect payment progress as it happens**, driven by SDK events: see the [send payment UX recommendations](/guide/send_payment.md#lightning-2). ### Contacts Sending should feel like **paying a person**, not pasting a string. Contacts (a name plus a Lightning address, synced across the user's devices by the SDK) are how the wallet gets there. See [Managing contacts](/guide/contacts.md). 1. **Contacts are people, so they live where people are paid**: inside the send flow, as a Contacts action next to Paste and Scan, and as autocomplete suggestions while the user types. Once chosen, show the contact (name and address) as a single clearable unit rather than raw text, and confirm as "Pay to {name}". 2. **Grow the contact list from real payments.** After a successful payment to a Lightning address the user hasn't saved, offer to save it: a non-blocking prompt after the send flow closes, with the name pre-filled from the address (`alice` for `alice@domain.com`). Never interrupt the payment itself. 3. **Only save addresses that work.** Before saving, verify the Lightning address actually resolves (using `parse`), not just that it looks valid. 4. **Trust the sync, but wait for it.** Refresh the contact list on each `SdkEvent::Synced` event so edits from other devices appear on their own, and don't show "no contacts yet" until the first sync has completed, so an existing user's contacts never look lost on a new device. ### Stable Balance [Stable Balance](/guide/stable_balance.md) means the wallet holds **a single balance, in either bitcoin or USD**. It is a denomination the user chooses, not a second account, and every screen should reflect that choice. 1. **One balance.** Show the balance in the active denomination only: USD when Stable Balance is active, sats otherwise. Never show two balances side by side. Residual sats below the conversion threshold appear as "change" under the USD balance. 2. **Switching denomination is switching the balance.** Let the user flip between BTC and USD from the balance itself, with a one-time explainer and the conversion fee shown before confirming. If a wallet holds a stablecoin balance while the mode is off (e.g. after restoring on another device), prompt to switch back to USD. 3. **The denomination carries through the flow.** When Stable Balance is active, amounts are entered and displayed in USD by default, with a switcher to sats. 4. **Conversions stay invisible until they cost something.** The user still sends and receives bitcoin; the SDK converts under the hood when preparing the payment. Disclose the conversion fee on the confirmation screen when the prepare response includes a `conversion_estimate`, and show "Converting..." before "Sending..." during execution. ### Sending USDC/USDT Frame [cross-chain sends](/guide/cross_chain.md) as **sending dollars**, not as moving crypto between chains. 1. **Same flow, different destination.** A pasted or scanned USDC/USDT address (EVM, Solana, or Tron, detected by `parse`) enters through the same unified send flow as everything else. No dedicated "cross-chain" screen. 2. **Dollars in, dollars out.** Denominate the amount in USD only; sats never appear in this flow. 3. **Ask only what can't be inferred.** When the destination is ambiguous, let the user pick the asset, then the network, then the provider, and skip any step that has a single option. Show the token contract address on request so careful users can verify the destination asset. 4. **Let providers compete in the open.** When multiple providers serve a route, show each one's receive amount and fee side by side and let the user choose. Hide providers that fail to produce a quote. 5. **Confirm with a full breakdown**: the amount the recipient receives, chain, provider, recipient address, and fee in the destination asset. Quotes expire; when one does, fetch a fresh one rather than sending on stale numbers. 6. **Sending is fast, delivery takes time.** The user's part completes quickly; the cross-chain delivery continues in the background. Show the payment as pending in the history until delivery completes, and rely on the SDK's automatic refund when a delivery fails. --- Identifier casing: `get_info` here is `getInfo` in Swift, Kotlin, JavaScript, React Native and Flutter, and `GetInfo` in Go and C#. Enum variants: `SdkEvent::Synced` is `SdkEvent.SYNCED` in Python, `SdkEvent.synced` in Swift, `SdkEventSynced` in Go, and `SdkEvent.Synced` elsewhere.