Integrating Stripe with Rust for WebAssembly Applications

April 4, 2025 ยท 7 min read

Introduction

Stripe is one of the most popular payment processing platforms for web applications, but officially they don't provide a Rust SDK. This presents a challenge for Rust developers looking to integrate Stripe payments into their applications, especially when building WebAssembly (WASM) applications for platforms like Cloudflare Workers.

In this blog post, we'll share how we integrated Stripe payments into our WIT Validator API billing module, which is built with Rust and deployed as a Cloudflare Worker using WebAssembly. This approach allows for a fully type-safe, memory-safe billing implementation with the performance benefits of Rust.

Why Use Rust for Stripe Integration?

While Stripe doesn't officially support Rust, there are several advantages to implementing Stripe integration in Rust:

Our Approach

Since Stripe doesn't provide a Rust SDK, we built our own lightweight client for the specific endpoints we needed. Our billing module supports:

Implementing a Rust Stripe Client

Here's the basic structure of our Stripe client module in Rust:

// stripe.rs
use serde::{Deserialize, Serialize};
use worker::*;

// Stripe API Configuration
const STRIPE_API_BASE: &str = "https://api.stripe.com/v1";

// Stripe Customer types
#[derive(Debug, Serialize, Deserialize)]
pub struct StripeCustomer {
    pub id: String,
    pub email: String,
}

// Stripe Checkout Session types
#[derive(Debug, Serialize, Deserialize)]
pub struct CheckoutSession {
    pub id: String,
    pub url: String,
}

// Create a Stripe customer
pub async fn create_customer(ctx: &RouteContext<()>, email: &str) -> Result {
    let secret_key = get_stripe_secret_key(ctx)?;
    
    let mut form_data = FormData::new();
    form_data.append("email", email)?;
    
    let mut headers = Headers::new();
    headers.set("Authorization", &format!("Bearer {}", secret_key))?;
    
    let request = Request::new_with_init(
        &format!("{}/customers", STRIPE_API_BASE),
        RequestInit::new()
            .with_method(Method::Post)
            .with_headers(headers)
            .with_body(Some(form_data.into())),
    )?;
    
    let mut response = Fetch::Request(request).send().await?;
    let result = response.json().await?;
    
    Ok(result)
}

// Create a checkout session
pub async fn create_checkout_session(
    ctx: &RouteContext<()>, 
    customer_id: &str,
    credits: u32,
) -> Result {
    let secret_key = get_stripe_secret_key(ctx)?;
    let success_url = "https://witgen.dev/billing/success?session_id={CHECKOUT_SESSION_ID}";
    let cancel_url = "https://witgen.dev/billing/checkout";
    
    let mut form_data = FormData::new();
    form_data.append("customer", customer_id)?;
    form_data.append("success_url", success_url)?;
    form_data.append("cancel_url", cancel_url)?;
    form_data.append("mode", "payment")?;
    form_data.append("line_items[0][price_data][currency]", "usd")?;
    form_data.append("line_items[0][price_data][product_data][name]", 
                     &format!("{} WIT Validation Credits", credits))?;
    form_data.append("line_items[0][price_data][unit_amount]", &format!("{}", credits * 100))?;
    form_data.append("line_items[0][quantity]", "1")?;
    
    let mut headers = Headers::new();
    headers.set("Authorization", &format!("Bearer {}", secret_key))?;
    
    let request = Request::new_with_init(
        &format!("{}/checkout/sessions", STRIPE_API_BASE),
        RequestInit::new()
            .with_method(Method::Post)
            .with_headers(headers)
            .with_body(Some(form_data.into())),
    )?;
    
    let mut response = Fetch::Request(request).send().await?;
    let result = response.json().await?;
    
    Ok(result)
}

// Helper function to get Stripe secret key from environment
fn get_stripe_secret_key(ctx: &RouteContext<()>) -> Result {
    ctx.var("STRIPE_SECRET_KEY")
        .map(|var| var.to_string())
        .map_err(|_| Error::from("Missing STRIPE_SECRET_KEY environment variable"))
}

// More functions for handling payments, webhooks, etc.

Creating the Billing API

With our Stripe client in place, we created a set of API endpoints to handle the billing flow:

// lib.rs (main worker entry point)
use worker::*;
mod stripe;
mod credits;

#[event(fetch)]
async fn main(req: Request, env: Env, ctx: Context) -> Result {
    let router = Router::new();
    
    router
        .post_async("/billing/signup", handle_signup)
        .post_async("/billing/create-checkout-session", handle_create_checkout)
        .get_async("/billing/checkout", handle_checkout_page)
        .get_async("/billing/success", handle_success_page)
        .post_async("/billing/process-payment", handle_process_payment)
        .get_async("/billing/credits", handle_get_credits)
        .post_async("/billing/use-credits", handle_use_credits)
        .post_async("/billing/webhook", handle_webhook)
        .run(req, env)
        .await
}

// Handler to create a checkout session
async fn handle_create_checkout(mut req: Request, ctx: RouteContext<()>) -> Result {
    // Parse the request body
    let data: serde_json::Value = req.json().await?;
    let email = match data.get("email").and_then(|e| e.as_str()) {
        Some(email) => email,
        None => return Response::error("Email is required", 400),
    };
    
    let credits = match data.get("credits").and_then(|c| c.as_u64()) {
        Some(credits) => credits as u32,
        None => return Response::error("Credits amount is required", 400),
    };
    
    // Get or create a customer
    let customer = match credits::get_customer(&ctx, email).await? {
        Some(customer) => customer,
        None => {
            // Create a new customer in Stripe
            let stripe_customer = stripe::create_customer(&ctx, email).await?;
            
            // Store the customer in our KV database
            credits::create_customer(&ctx, email, &stripe_customer.id).await?;
            
            stripe_customer
        }
    };
    
    // Create a checkout session
    let session = stripe::create_checkout_session(&ctx, &customer.id, credits).await?;
    
    // Return the checkout session URL
    Response::from_json(&serde_json::json!({
        "url": session.url
    }))
}

// Other handler implementations...

Storing and Managing Credits

We use Cloudflare's KV storage to track user credits:

// credits.rs
use serde::{Deserialize, Serialize};
use worker::*;

// User data structure
#[derive(Debug, Serialize, Deserialize)]
pub struct User {
    pub email: String,
    pub stripe_customer_id: String,
    pub credits: u32,
}

// Get a user from KV storage
pub async fn get_user(ctx: &RouteContext<()>, email: &str) -> Result> {
    let kv = ctx.kv("USERS")?;
    let user = kv.get(email).json::().await?;
    Ok(user)
}

// Add credits to a user account
pub async fn add_credits(ctx: &RouteContext<()>, email: &str, amount: u32) -> Result {
    let kv = ctx.kv("USERS")?;
    
    // Get existing user or return error
    let mut user = match get_user(ctx, email).await? {
        Some(user) => user,
        None => return Err(Error::from("User not found")),
    };
    
    // Add credits
    user.credits += amount;
    
    // Save updated user
    kv.put(email, user.clone())?.execute().await?;
    
    Ok(user)
}

// Use credits from a user account
pub async fn use_credits(ctx: &RouteContext<()>, email: &str, amount: u32) -> Result {
    let kv = ctx.kv("USERS")?;
    
    // Get existing user or return error
    let mut user = match get_user(ctx, email).await? {
        Some(user) => user,
        None => return Err(Error::from("User not found")),
    };
    
    // Check if user has enough credits
    if user.credits < amount {
        return Err(Error::from("Insufficient credits"));
    }
    
    // Deduct credits
    user.credits -= amount;
    
    // Save updated user
    kv.put(email, user.clone())?.execute().await?;
    
    Ok(user)
}

Handling Stripe Webhooks

To properly process payments and update credit balances, we implemented webhook handling:

// Webhook handler in lib.rs
async fn handle_webhook(mut req: Request, ctx: RouteContext<()>) -> Result {
    // Get the Stripe webhook secret
    let webhook_secret = ctx.var("STRIPE_WEBHOOK_SECRET")?.to_string();
    
    // Get the Stripe signature from headers
    let signature = match req.headers().get("Stripe-Signature")? {
        Some(sig) => sig,
        None => return Response::error("Missing Stripe signature", 400),
    };
    
    // Get the raw body
    let body = req.text().await?;
    
    // Verify the webhook signature (simplified example)
    if !stripe::verify_webhook_signature(&body, &signature, &webhook_secret)? {
        return Response::error("Invalid webhook signature", 400);
    }
    
    // Parse the event
    let event: stripe::Event = serde_json::from_str(&body)?;
    
    // Handle different event types
    match event.type_field.as_str() {
        "checkout.session.completed" => {
            // Extract checkout session data
            let session: stripe::CheckoutSession = serde_json::from_value(event.data.object)?;
            
            // Get customer email from metadata or customer object
            let email = session.customer_email.unwrap_or_default();
            if email.is_empty() {
                return Response::error("Missing customer email", 400);
            }
            
            // Get amount paid and convert to credits (simplified example)
            let amount_paid = session.amount_total.unwrap_or(0) / 100;
            let credits = amount_paid as u32;
            
            // Add credits to the user's account
            credits::add_credits(&ctx, &email, credits).await?;
            
            // Return success
            Response::ok("Credits added successfully")
        },
        // Handle other event types
        _ => Response::ok("Event received but not processed"),
    }
}

Integration with Other Applications

One of the key advantages of our approach is that any Rust application can integrate with our billing API. Here's a simple example of how to validate WIT files using credits:

async fn validate_with_billing(wit_content: &str, email: &str) -> Result {
    // Check if user has credits
    let credits_url = "https://witgen.dev/billing/credits";
    let credits_resp = reqwest::Client::new()
        .get(format!("{}?email={}", credits_url, email))
        .send()
        .await?;
        
    let credits_data: CreditResponse = credits_resp.json().await?;
    
    if credits_data.credits < 1 {
        return Err(Error::InsufficientCredits);
    }
    
    // Use credits if available
    let use_credits_url = "https://witgen.dev/billing/use-credits";
    reqwest::Client::new()
        .post(use_credits_url)
        .json(&json!({
            "email": email,
            "credits": 1
        }))
        .send()
        .await?;
        
    // Proceed with validation
    let validation_result = validate_wit(wit_content).await?;
    
    Ok(validation_result)
}

Testing the System

Testing a payment system is critical, and Stripe provides excellent testing capabilities. We've documented the process in our README:

  1. Visit https://witgen.dev/billing/checkout?email=your.email@example.com&credits=100
  2. Use Stripe test cards:
    • Success: 4242 4242 4242 4242 (any future expiry date, any CVC)
    • Decline: 4000 0000 0000 0002
    • 3D Secure: 4000 0000 0000 3220 (requires authentication)
  3. Verify credits at https://witgen.dev/billing/credits?email=your.email@example.com

Challenges and Solutions

Building a Stripe integration with Rust presented several challenges:

Conclusion

While Stripe doesn't officially support Rust, it's entirely possible to build a robust, type-safe Stripe integration using Rust for WebAssembly applications. Our billing module has been working reliably in production, processing payments and managing user credits for the WIT Validator API.

By handling our own Stripe API integration, we've been able to keep our entire stack in Rust, avoiding the need to introduce JavaScript or another language just for payment processing. This approach has given us excellent performance, strong type safety, and a more cohesive codebase.

If you're interested in exploring our implementation further, check out the WIT Validator API on GitHub.