Integrating Stripe with Rust for WebAssembly Applications
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:
- Type safety and memory safety across your entire application
- Excellent performance characteristics, important for billing systems
- Strong concurrency model for handling multiple payment requests
- WebAssembly compatibility for edge deployment (like Cloudflare Workers)
- No context switching between languages in your codebase
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:
- Creating and managing Stripe customers
- Generating checkout sessions
- Processing payments
- Handling webhooks for payment events
- Managing user credits after successful payments
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
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:
- Visit
https://witgen.dev/billing/checkout?email=your.email@example.com&credits=100
- 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)
- Success:
- 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:
- No official SDK: We had to build our own lightweight client for the API endpoints we needed.
- Type definitions: We created our own Rust types matching Stripe's API responses.
- Webhook verification: Implementing proper signature verification required careful handling of HMAC signatures.
- FormData handling: Working with nested form data for Stripe's API required careful construction.
- Error handling: We built robust error handling to ensure financial transactions were always consistent.
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.