This guide walks you through the setup of Para in a Flutter application. You’ll learn how to install the SDK and configure iOS/Android for passkey-based logins , implement user flows for email, phone, or OAuth, and generate wallets and sign transactions.

If you haven’t already created your Flutter app, follow the official Flutter docs to set up a new project.

Prerequisites

To use Para, you need an API key. This key authenticates your requests to Para 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.

Installation

Start by installing the Para SDK:

flutter pub add para

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.getpara.com
    • webcredentials:app.getpara.com

For more details, see the Apple Developer documentation.

Important: You must register your teamId + bundleIdentifier with the Para 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 Para. This is required by Apple for passkey security. Allow up to 24 hours for domain propagation.

Initializing Para

To use Para’s features, you’ll need to initialize a Para client instance that can be accessed throughout your app. This client handles all interactions with Para’s services, including authentication, wallet management, and transaction signing.

First, create a file (e.g., lib/services/para_client.dart) to initialize your Para client:

import 'package:para/para.dart';

// Initialize a global Para client instance
final para = Para(
  environment: Environment.beta, // Use Environment.prod for production
  apiKey: 'YOUR_PARA_API_KEY',
);

Next, ensure Flutter’s widgets are properly initialized before your app starts. Update your main.dart:

import 'package:flutter/material.dart';
import 'services/para_client.dart';

void main() {
  // This is required for Flutter initialization
  WidgetsFlutterBinding.ensureInitialized();

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Your App Name',
      home: YourHomeScreen(), // Your initial screen
    );
  }
}

You can access para from anywhere in your app by importing the file where you initialized it. This singleton pattern ensures consistent state management across your application.

Authentication

Para provides multiple authentication methods including email, phone, and OAuth. The email and phone flows follow a similar pattern but with slightly different method calls. Here’s a detailed breakdown of each step in the authentication process:

User Creation

Beta Testing Credentials In the BETA Environment, you can use any email ending in @test.getpara.com (like dev@test.getpara.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.

// First, check if a user account exists with this email
if (await para.checkIfUserExists({ email })) {
  // If the user exists, direct them to the login flow
  await handleExistingUser();
} else {
  // Create a new user account - this automatically sends a verification code
  await para.createUser({ email });
  
  // Collect the verification code sent to their email
  // This step requires UI to input the code
  final otpCode = await collectOTPFromUser();
  
  // Verify the code - on success this returns a biometricsId 
  // needed for passkey generation
  final biometricsId = await para.verifyEmail(otpCode);

  // Generate a passkey for this device using the biometricsId
  // This triggers the platform's native passkey UI
  await para.generatePasskey({ email, biometricsId });

  // Create a wallet for the user
  // By default this creates a wallet of the type configured in your developer portal
  final result = await para.createWallet({ skipDistribute: false });
}

The verification code (OTP) is automatically sent when calling createUser or createUserByPhone. You’ll need to implement UI to collect this code from the user before proceeding with verification.

When createWallet is called with skipDistribute: false, Para automatically handles the distribution of backup shares. Seucrely show this information to the user in your app.

User Login

Logging in with Para is straightforward using the login() method. When called, this triggers the platform’s native passkey selection UI, allowing users to authenticate with any passkeys associated with your app on their device:

try {
  final wallet = await para.login();
  print('Logged in with wallet address: ${wallet.address}');
} catch (e) {
  print('Login failed: ${e.toString()}');
}

The login method returns the user’s primary wallet. If you need access to additional wallets, you can fetch them using para.getWallets().

Transaction Signing

Once a user is authenticated, Para provides three methods for signing operations:

  1. signMessage - Signs arbitrary data
  2. signTransaction - Signs EVM-specific transactions (requires RLP encoding)
  3. sendTransaction - Signs and broadcasts RLP-encoded transactions directly

Here’s how to use each method:

General Message Signing

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

final messageBytes = utf8.encode('Hello World');
final base64Message = base64Encode(messageBytes);

final signatureResult = await para.signMessage({
  walletId: wallet.id,
  messageBase64: base64Message
});

Transaction Signing

For blockchain transactions, you’ll typically:

  1. Construct the transaction using a chain-specific library
  2. Serialize it to bytes
  3. Convert to base64
  4. Sign using Para
// Example using Solana
final transaction = Transaction();
// ... configure transaction

final messageBytes = transaction.serializeMessage();
final base64Transaction = base64Encode(messageBytes);

final signatureResult = await para.signMessage({
  walletId: wallet.id,
  messageBase64: base64Transaction
});

Direct Transaction Submission

For EVM chains, you can use sendTransaction to sign and submit in one call:

final signatureResult = await para.sendTransaction({
  walletId: wallet.id,
  rlpEncodedTxBase64: base64EncodedTransaction,
  chainId: '1' // Mainnet
});

For EVM transactions, consider using signTransaction instead of signMessage as it provides additional validation specific to EVM chains.

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.