0. Prerequisites

This guide assumes that you have completed the Setup guide to get a Privy client instance.

1. Creating a wallet

First, we will create a wallet. You will use this wallet’s id in future calls to sign messages and send transactions.
use privy_rs::{PrivyClient, generated::types::{CreateWalletBody, WalletChainType}};

let wallet = client
    .wallets()
    .create(
        None, // No owner - creates unowned wallet
        &CreateWalletBody {
            chain_type: WalletChainType::Ethereum,
            additional_signers: None,
            owner: None,
            owner_id: None,
            policy_ids: vec![],
        },
    )
    .await?;

let wallet_id = wallet.id;
Learn more about creating wallets.
When using the PrivyClient to work with the API, all errors thrown will be instances of privy_rs::Error. This error type implements std::error::Error, so you can use it with ? to propagate errors or your favorite error handling library such as anyhow. You can see more examples in the error handling section below.

2. Signing a message

Next, we’ll sign a plaintext message with the wallet using chain-specific signing methods. Make sure to specify your wallet ID from creation in the input.
use privy_rs::{AuthorizationContext, PrivateKey};

// Create authorization context with your private key
let private_key_pem = std::fs::read_to_string("private_key.pem")?;
let key = PrivateKey(private_key_pem);
let ctx = AuthorizationContext::new().push(key);

let message = "Hello, Privy!";
let response = client
    .wallets()
    .ethereum()
    .sign_message(&wallet_id, message, &ctx, None)
    .await?;

// Signature is hex-encoded for Ethereum
let signature = response.signature;
Learn more about signing messages.

3. Sending transactions

Your wallet must have some funds in order to send a transaction. You can use a testnet faucet to test transacting on a testnet (e.g. Base Sepolia) or send funds to the wallet on the network of your choice.
To send a transaction from your wallet, use chain-specific transaction methods. The SDK will populate missing network-related values, sign your transaction, broadcast it to the network, and return the transaction hash to you. In the request, make sure to specify your wallet id from your wallet creation above, as well as the caip2 chain ID for the network you want to transact on.
use privy_rs::{AuthorizationContext, PrivateKey, generated::types::*};

// Create authorization context with your private key
let private_key_pem = std::fs::read_to_string("private_key.pem")?;
let key = PrivateKey(private_key_pem);
let ctx = AuthorizationContext::new().push(key);

let caip2 = "eip155:11155111"; // Sepolia testnet
let recipient_address = "0x742d35Cc6635C0532925a3b8c17d6d1E9C2F7ca"; // Your recipient address

let transaction = EthereumSendTransactionRpcInputParamsTransaction {
    to: Some(recipient_address.to_string()),
    value: Some("0x1".to_string()), // 1 wei
    gas_limit: None,
    max_fee_per_gas: None,
    max_priority_fee_per_gas: None,
    data: Some("0x".to_string()),
    chain_id: Some(11155111), // Sepolia testnet
    from: None,
    gas_price: None,
    nonce: None,
    type_: None,
};

let response = client
    .wallets()
    .ethereum()
    .send_transaction(&wallet_id, caip2, transaction, &ctx, None)
    .await?;

let transaction_hash = response.hash;
Learn more about sending transactions.
If you’re interested in more control, you can prepare and broadcast the transaction yourself, and simply use raw signing methods to sign the transaction with a wallet.

4. Creating a user

To create a user for your application, you can use the create method, passing in linked accounts, custom metadata, and wallets that should be associated with said user.
use privy_rs::generated::types::{
    CreateUserBody, LinkedAccountEmailInput,
    LinkedAccountEmailInputType, LinkedAccountInput,
    LinkedAccountCustomAuthInput, LinkedAccountCustomAuthInputType,
};

let user = client
    .users()
    .create(&CreateUserBody {
        linked_accounts: vec![
            LinkedAccountInput::CustomAuthInput(LinkedAccountCustomAuthInput {
                custom_user_id: "your-subject-id".to_string(),
                type_: LinkedAccountCustomAuthInputType::CustomAuth,
            }),
            LinkedAccountInput::EmailInput(LinkedAccountEmailInput {
                address: "[email protected]".to_string(),
                type_: LinkedAccountEmailInputType::Email,
            }),
        ],
        create_embedded_wallet: Some(false),
        custom_metadata: None,
    })
    .await?;

let user_id = user.id;
Learn more about creating users, and look at our pregenerating wallets guide for linking wallets to your users before they even sign in.

5. Error handling

In the examples above we use ? to propagate errors, however the Rust SDK provides comprehensive error handling through multiple specialized error types. All error types implement std::error::Error, allowing seamless integration with Rust’s error handling ecosystem.

Error types

The SDK provides the following main error categories:

PrivyApiError

The core generated client error type that represents all possible API communication failures:
Error VariantDescriptionWhen It Occurs
InvalidRequestRequest doesn’t conform to API requirementsMissing required fields, invalid data formats
CommunicationErrorNetwork or connection issuesNetwork timeouts, DNS failures, connection drops
InvalidUpgradeConnection upgrade failedWebSocket or HTTP/2 upgrade errors
ErrorResponseDocumented API error responseAuthentication failures, insufficient permissions
ResponseBodyErrorFailed to read response bodyCorrupted response data
InvalidResponsePayloadResponse deserialization failedUnexpected response format
UnexpectedResponseUndocumented response codeNew API responses not yet supported
CustomConsumer-defined hook errorCustom validation or processing failures

PrivyCreateError

Client initialization errors that occur when creating a PrivyClient instance:
Error VariantDescriptionWhen It Occurs
InvalidHeaderValueInvalid HTTP header value providedMalformed app credentials
ClientHTTP client creation failedNetwork configuration issues

PrivySignedApiError

Errors for operations requiring cryptographic signatures (authorization keys):
Error VariantDescriptionWhen It Occurs
ApiPrivy API returned an error responseAuthentication failures, rate limits
SignatureGenerationFailed to generate request signatureInvalid private key, signing errors

PrivyExportError

Wallet export operation errors:
Error VariantDescriptionWhen It Occurs
ApiPrivy API returned an error responseExport permissions, wallet access
SignatureGenerationFailed to generate request signatureInvalid authorization key
KeyDecryption or key handling failedInvalid encryption key, corrupted data

CryptoError

General cryptographic operation errors:
Error VariantDescriptionWhen It Occurs
SigningDigital signature operation failedInvalid key, signature algorithm issues
KeyKey handling operation failedKey parsing, loading, or format errors

KeyError

Cryptographic key management errors:
Error VariantDescriptionWhen It Occurs
IoFile I/O error reading keyMissing key file, permission denied
InvalidFormatKey data is malformedInvalid PEM/DER format, corrupted data
HpkeDecryptionHPKE decryption failedWrong decryption key, corrupted payload
OtherUnknown error occurredUnexpected system errors

SigningError

Digital signature creation errors:
Error VariantDescriptionWhen It Occurs
KeyInvalid signing keyWrong key type, corrupted key data
SignatureCryptographic signing failedAlgorithm errors, hardware issues
OtherUnknown signing errorUnexpected cryptographic errors

SignatureGenerationError

Authorization signature generation errors:
Error VariantDescriptionWhen It Occurs
SerializationRequest serialization failedInvalid request data structure
SigningSigning process failedKey errors, cryptographic failures

Basic error handling

use privy_rs::{PrivyClient, PrivyCreateError, PrivyApiError, generated::types::*};

// Using the ? operator for error propagation
async fn create_wallet_with_basic_handling() -> Result<String, Box<dyn std::error::Error>> {
    let client = PrivyClient::new("your-app-id", "your-app-secret")?;

    let wallet = client
        .wallets()
        .create(
            None,
            &CreateWalletBody {
                chain_type: WalletChainType::Ethereum,
                additional_signers: None,
                owner: None,
                owner_id: None,
                policy_ids: vec![],
            },
        )
        .await?;

    Ok(wallet.id)
}

Advanced error handling

use privy_rs::{PrivyClient, PrivyCreateError, PrivySignedApiError, generated::types::*};

async fn create_wallet_with_detailed_handling() -> Result<String, String> {
    // Handle client creation errors
    let client = match PrivyClient::new("your-app-id", "your-app-secret") {
        Ok(client) => client,
        Err(PrivyCreateError::InvalidHeaderValue(e)) => {
            return Err(format!("Invalid credentials format: {}", e));
        }
        Err(PrivyCreateError::Client(e)) => {
            return Err(format!("HTTP client creation failed: {}", e));
        }
    };

    // Handle wallet creation errors
    match client
        .wallets()
        .create(
            None,
            &CreateWalletBody {
                chain_type: WalletChainType::Ethereum,
                additional_signers: None,
                owner: None,
                owner_id: None,
                policy_ids: vec![],
            },
        )
        .await
    {
        Ok(wallet) => Ok(wallet.id),
        Err(e) => {
            // e is of type PrivyApiError here
            match e {
                PrivyApiError::InvalidRequest(msg) => {
                    Err(format!("Invalid request: {}", msg))
                }
                PrivyApiError::CommunicationError(req_err) => {
                    Err(format!("Network error: {}", req_err))
                }
                PrivyApiError::ErrorResponse(response) => {
                    Err(format!("API error response: {:?}", response))
                }
                _ => Err(format!("Unexpected error: {}", e)),
            }
        }
    }
}

// Example with authorization key signing
async fn sign_with_auth_key() -> Result<String, String> {
    use privy_rs::{AuthorizationContext, PrivateKey};

    let client = PrivyClient::new("your-app-id", "your-app-secret")
        .map_err(|e| format!("Client creation failed: {}", e))?;

    let private_key_pem = std::fs::read_to_string("private_key.pem")
        .map_err(|e| format!("Failed to read private key: {}", e))?;

    let key = PrivateKey(private_key_pem);
    let ctx = AuthorizationContext::new().push(key);

    let wallet_id = "wallet-id";
    let message = "Hello, Privy!";

    match client
        .wallets()
        .ethereum()
        .sign_message(wallet_id, message, &ctx, None)
        .await
    {
        Ok(response) => Ok(response.signature),
        Err(PrivySignedApiError::Api(api_err)) => {
            Err(format!("API error: {}", api_err))
        }
        Err(PrivySignedApiError::SignatureGeneration(sig_err)) => {
            Err(format!("Signature generation failed: {}", sig_err))
        }
    }
}

Integration with error handling libraries

The SDK works seamlessly with popular Rust error handling libraries:
use anyhow::Result;
use privy_rs::PrivyClient;

async fn create_wallet() -> Result<String> {
    let client = PrivyClient::new("your-app-id", "your-app-secret")?;

    let wallet = client
        .wallets()
        .create(None, &create_wallet_body)
        .await?;

    Ok(wallet.id)
}
We also implement logging via the tracing crate, which is a popular and well-supported logging facade for Rust.

Next steps & advanced topics

  • For an additional layer of security, you can choose to sign your requests with authorization keys.
  • To restrict what wallets can do, you can set up policies.
  • To prevent double sending the same transaction, take a look at our support for idempotency keys.
  • If you want to require multiple parties to sign off before sending a transaction for a wallet, you can accomplish this through the use of quorum approvals.