The Capsule SDK for Flutter allows you to easily integrate secure and scalable wallet functionalities into your mobile applications. This guide covers the installation, setup, and usage of the Capsule SDK, including handling authentication flows.

Prerequisites

To use Capsule, you need an API key. This key authenticates your requests to Capsule services and is essential for integration.

Don’t have an API key yet? Request access to the Developer Portal to create API keys, manage billing, teams, and more.

Dependency Installation

To install the Capsule SDK and required dependencies, use the following command:

flutter pub add capsule

Project Setup

To set up associated domains for passkey functionality in your Flutter project, you need to configure both iOS and Android platforms:

To enable passkeys on iOS, you need to set up associated domains in your Xcode project:

  1. Open your Flutter project’s iOS folder in Xcode
  2. Select your target and go to “Signing & Capabilities”
  3. Click ”+ Capability” and add “Associated Domains”
  4. Add the following domains:
    • webcredentials:app.beta.usecapsule.com
    • webcredentials:app.usecapsule.com

For more details, see the Apple Developer documentation.

Important: You must register your teamId + bundleIdentifier with the Capsule team to set up associated domains. For example, if your Team ID is A1B2C3D4E5 and Bundle Identifier is com.yourdomain.yourapp, provide A1B2C3D4E5.com.yourdomain.yourapp to Capsule. This is required by Apple for passkey security. Allow up to 24 hours for domain propagation.

Using the Capsule SDK

Much like our web SDKs, the Flutter SDK follows a similar usage pattern, handling both authentication and signing capabilities. The SDK provides comprehensive authentication flows for both creating new users and logging in existing users. Both flows utilize passkeys for secure and seamless authentication.

For authentication, we have two main paths: creating a new user or logging in an existing one. Authentication options include phone, email, OAuth, and an advanced use case with pregenerated wallets (covered later). The key concept is that once a user is created, we create an associated native passkey on their mobile device. This passkey will secure the user’s wallets when they’re created.

Beta Testing Credentials In the BETA Environment, you can use any email ending in @test.usecapsule.com (like dev@test.usecapsule.com) or US phone numbers (+1) in the format (area code)-555-xxxx (like (425)-555-1234). Any OTP code will work for verification with these test credentials. These credentials are for beta testing only. You can delete test users anytime in the beta developer console to free up user slots.

Capsule Initialization

First things first - you’ll need a beta API key from the developer portal. Once you’ve got your API key, you can create a Capsule instance in your Flutter application. Here’s an example of how to set this up:

import 'package:capsule/capsule.dart';

final capsuleClient = Capsule(
  environment: Environment.sandbox, // or Environment.prod
  apiKey: 'your_api_key_here',
);

While we recommend utilizing environment variables for API keys, there’s no harm in the keys being exposed on the client side.

Authentication Methods

The SDK provides multiple authentication methods, each following a similar pattern but with their own specific requirements:

  • Email + Passkey
  • Phone + Passkey
  • OAuth (Google, Apple, X/Twitter, Discord)
  • Pregenerated Wallets (advanced use case)

Email Authentication Flow

The email flow demonstrates the typical pattern all authentication methods follow. Here’s a detailed walkthrough:

1

Check If User Exists

First, we need to check if the user already has an account. This determines whether we proceed with creation or direct them to login:

// Get the user's email from your UI
final email = emailController.text.trim();

try {
  final exists = await capsuleClient.checkIfUserExists(email);
  if (exists) {
    // Show UI prompt to use passkey login instead
    // e.g., showDialog or navigate to login screen
    return;
  }
} catch (e) {
  // Handle errors appropriately in your UI
  // e.g., show error message to user
  return;
}
2

Create User and Send OTP

For new users, we initiate the account creation process. This triggers an OTP email:

try {
  await capsuleClient.createUser(email);

  // After successful creation:
  // 1. Inform user to check their email
  // 2. Show OTP entry UI
} catch (e) {
  // Handle creation errors
  // Show appropriate error message to user
}

The OTP email is automatically sent to the provided address. You don’t need to trigger this separately.

3

Verify OTP and Generate Passkey

Once the user enters the OTP, we verify it and create their passkey:

// Get OTP code from your UI
final otpCode = otpController.text.trim();

try {
  // Verify the email with the OTP
  final biometricsId = await capsuleClient.verifyEmail(otpCode);

  // Generate the passkey using the verified credentials
  await capsuleClient.generatePasskey(email, biometricsId);

  // At this point:
  // 1. Show success message
  // 2. Indicate that wallet creation is next
  // 3. You might want to show a loading indicator
} catch (e) {
  // Handle verification errors
  // Consider showing a retry option
  return;
}

The passkey generation will trigger the device’s native biometric prompt. No additional UI is needed for this step.

4

Create and Secure Wallet

Finally, we create the user’s wallet:

try {
  final result = await capsuleClient.createWallet(skipDistribute: false);

  // Important: Store or display the recovery secret
  final recoverySecret = result.recoverySecret;
  final wallet = result.wallet;

  // At this point you should:
  // 1. Suggest secure storage of the recovery secret
  // 2. Show the wallet address to the user
  // 3. Navigate to your app's main interface
} catch (e) {
  // Handle wallet creation errors
  return;
}

The recoverySecret is crucial for account recovery. Ensure you have a secure way to store or display it to the user.

Phone Authentication Flow

The phone flow follows the same pattern as email but with phone-specific methods:

1

Check If User Exists

// Get phone details from your UI
final phone = phoneController.text.trim();
final countryCode = countryCodeController.text.trim();

try {
  final exists = await capsuleClient.checkIfUserExistsByPhone(
    phone,
    '+${countryCode}' // Must include + prefix
  );
  
  if (exists) {
    // Direct to passkey login
    return;
  }
} catch (e) {
  // Handle check error
  return;
}
2

Create User and Send OTP

try {
  await capsuleClient.createUserByPhone(
    phone,
    '+${countryCode}'
  );
  
  // Show OTP entry UI
  // Consider implementing SMS auto-fill if available
} catch (e) {
  // Handle creation error
}

Country code requires the + prefix. Ensure you include this in the phone number before sending it to the SDK.

3

Verify OTP and Generate Passkey

try {
  final biometricsId = await capsuleClient.verifyPhone(otpCode);
  
  // The full phone number must include country code
  final fullPhone = '+${countryCode}${phone}';
  await capsuleClient.generatePasskey(fullPhone, biometricsId);
  
  // Show success indication
} catch (e) {
  // Handle verification error
}
4

Create Wallet

This step is identical to the email flow’s wallet creation step.

OAuth Authentication Flow

OAuth provides a streamlined process, handling much of the verification automatically, but unlike email and phone requires a browser view for the initial authentication:

1

Initiate OAuth Flow

try {
  // Get OAuth URL for selected provider
  final oauthUrl = await capsuleClient.getOAuthURL(provider);

  // Start polling for OAuth completion
  final oauthFuture = capsuleClient.waitForOAuth();

  // Launch OAuth URL in browser
  // This example uses flutter_inappwebview
  final browser = ChromeSafariBrowser();
  await browser.open(url: WebUri(oauthUrl));

  //await the OAuth result future to close the browser
  final oauthResult = await oauthFuture;

  try {
    await browser.close();
  } catch (_) {}

  if (oauthResult.isError == true) {
    throw Exception('OAuth authentication failed');
  }
} catch (e) {
  // Handle OAuth initiation errors
  return;
}

The OAuth URL should be opened in a secure browser view for better security but a WebView in your app is also an option.

We start polling for OAuth completion before opening the browser to ensure we catch the result.
2

Handle OAuth Result

try {
  if (oauthResult.userExists) {
    // Existing user - proceed to passkey login
    final wallet = await capsuleClient.login();
    // Navigate to main app interface
  } else {
    // New user - create passkey and wallet
    if (!oauthResult.email) {
      throw Exception('Email required for registration');
    }
    
    final biometricsId = await capsuleClient.verifyOAuth();
    await capsuleClient.generatePasskey(oauthResult.email!, biometricsId);
    
    // Create wallet like in other flows
    final result = await capsuleClient.createWallet(skipDistribute: false);
    // Handle wallet and recovery secret
  }
} catch (e) {
  // Handle OAuth completion errors
}

OAuth flows automatically retrieve the user’s email. No manual email collection is needed. The flow will be the same as email authentication after the OAuth result is received.

Each flow ultimately results in:

  1. A verified user account
  2. A device-specific passkey
  3. A wallet with its recovery secret

The key difference between flows is the initial verification method, but they all converge at the passkey and wallet creation steps.

Login Existing User

Once the wallet is created and automatically stored and secured by the passkey, logging in becomes straightforward. The Capsule Flutter class mirrors most of the same method names as the Capsule web JS class, maintaining consistency across platforms.

1

Initiate User Login

The login process is simplified into a single method that handles the passkey authentication handshake with Capsule servers and securely loads the user’s wallet:

Wallet wallet;
try {
  wallet = await _capsule.login();
  print("User logged in successfully");
} catch (e) {
  print("Error during login: \$e");
  return;
}

// Handle wallet information after successful login
final address = wallet.address;
print("Wallet address: \$address");

Transaction Signing

Now that we have a user with wallets, we can begin signing. Signing with Capsule provides three main methods:

  1. signMessage - For general-purpose signing of any data
  2. signTransaction - Specifically for EVM transactions (requires RLP encoding)
  3. sendTransaction - For direct transaction submission of RLPEncoded transactions

Here’s the important part: signMessage signs on raw bytes of a base64 string. It will sign ANY data that’s passed into the message parameter. Remember - it signs on the raw bytes, so the data itself isn’t relevant. This means transactions, messages, and other data types need to be properly constructed before passing them to signMessage. There’s no validation when signing raw bytes - we sign what you give us.

This is why it’s crucial to use libraries that handle Transaction construction correctly. You’ll want to:

  1. Construct the transaction
  2. Get the serialized byte data
  3. Convert it to base64
  4. Pass that into signMessage

signMessage works with raw bytes in base64 format. It will sign ANY provided data without validation, so ensure your transaction construction is correct before signing.

Here’s an example in Solana using the solana_web3 package:

import 'package:solana_web3/solana_web3.dart' as web3;

// Construct the transaction
final transaction = web3.Transaction.v0(
  payer: publicKey,
  recentBlockhash: blockhash,
  instructions: [
    programs.SystemProgram.transfer(
      fromPubkey: publicKey,
      toPubkey: recipientPubkey,
      lamports: amount,
    ),
  ],
);

// Get bytes and convert to base64
final message = Uint8List.fromList(transaction.serializeMessage());
final messageBase64 = base64Encode(message);

// Sign with Capsule
final result = await capsuleClient.signMessage(
  walletId: wallet.id,
  messageBase64: messageBase64,
);

if (result is SuccessfulSignatureResult) {
  final signature = base64.decode(result.signature);
  transaction.addSignature(publicKey, signature);
}
More examples for EVM and Cosmos will be added soon.

Advanced: Pregenerated Wallets

Pregenerated wallets are an advanced use case that lets you create a wallet based on an identifier like an email before the user exists. When you create a pregenerated wallet, your app receives the user’s half of the key (userShare). This keyShare must be secured, encrypted, and NOT lost as it controls the user’s half of the key.

This advanced feature allows apps to take actions on behalf of a user without needing user interaction. The methods are identical across all our SDKs, so you can use them the same way in Flutter as you would in Web or Server implementations:

// Create pregen wallet
final wallet = await capsuleClient.createWalletPreGen(
  type: WalletType.evm,
  pregenIdentifier: 'user@example.com',
  pregenIdentifierType: PregenIdentifierType.email,
);

// Get user's key share - needs secure storage
final userShare = await capsuleClient.getUserShare();

// Set stored user share when needed
await capsuleClient.setUserShare(userShare);
The userShare must be securely stored and not lost - it controls the user’s half of the key.

For more details on pregenerated wallets check out the

Pregenerated Wallets guide.

Examples

To help you get started with the Capsule Flutter SDK, we’ve prepared a comprehensive example:

This example repository demonstrates how to handle user authentication, wallet creation, and signing messages using the Capsule SDK in a Flutter application.

Next Steps

After integrating Capsule, you can explore other features and integrations to enhance your Capsule experience. Here are some resources to help you get started:

Ecosystems

Learn how to use Capsule with popular Web3 clients and wallet connectors. We’ll cover integration with key libraries for EVM, Solana, and Cosmos ecosystems.

If you’re ready to go live with your Capsule integration, make sure to review our go-live checklist:

Troubleshooting

If you encounter issues during the integration or usage of the Capsule SDK in your Flutter application, here are some common problems and their solutions:

For more detailed troubleshooting and solutions specific to Flutter, please refer to our comprehensive troubleshooting guide:

Integration Support

If you’re experiencing issues that aren’t resolved by our troubleshooting resources, please contact our team for assistance. To help us resolve your issue quickly, please include the following information in your request:

  1. 1

    A detailed description of the problem you’re encountering.

  2. 2

    Any relevant error messages or logs.

  3. 3

    Steps to reproduce the issue.

  4. 4

    Details about your system or environment (e.g., device, operating system, software version).

Providing this information will enable our team to address your concerns more efficiently.