Using Stripe Payments with Rust

Cover image

Hello world! In this article we’re going to talk about how to integrate Stripe Payments into a Rust application. Stripe is a hassle-free, easy to use payments provider that makes commercializing your Rust web services hassle-free. We’re going to cover:

  • One off payments
  • Setting up customers
  • Products and prices, plus subscriptions (creating, updating and deleting)
  • Webhooks

Getting started

To get started, we’ll grab a Stripe API key from the dashboard. Stripe is free to sign up for and try out in the Test Mode. You can find out more about this here.

To use Stripe with Rust, we’ll add the following dependency (async-stripe) to our project with this shell snippet:

cargo add async-stripe

Using Stripe

Products

Before we can do anything meaningful, we are required to create Stripe products. The product data should be stored on Stripe as a source of truth for us to be able to generate purchases and/or product prices programmatically. On our side, we will only store the product and price object IDs. This gives us two advantages:

  • The IDs aren’t useful by themselves; if our database gets compromised, the information cannot be meaningfully used.
  • We can rely on Stripe as a source of truth instead of having two sources of information (though if there’s anything you want to store for convenience like product names, etc, that’s probably a good idea!).

Let’s have a look at creating a product:

let stripe_key = std::env::var("STRIPE_API_KEY").expect("STRIPE_API_KEY env var not found");
let client = stripe::Client::new(stripe_key);

// create a new example project
let product = {
    let mut create_product = CreateProduct::new("T-Shirt");
    create_product.metadata = Some(
        std::collections::HashMap::from([(String::from("async-stripe"), String::from("true"))])
    );
    Product::create(&client, create_product).await.unwrap()
};

Note here that you can add any metadata you want for your product (as long as it can be a String). If you need to add things like net weight, nutritional information, or other kinds of information this is where you’d put it.

Additionally, you can add images to the CreateProduct struct - up to 8 using URLs. This would mean using something like Cloudflare R2 or AWS S3 buckets to store your images and accessing them from there.

You can read more about the CreateProduct struct here.

Prices in Stripe are essentially separate objects from products themselves. This allows us to create multiple prices for a project and price tiering. we’ll need to do use a CreatePrice struct that requires an additional product. Because the price and product objects are separate, we can create multiple prices for the same product!

let price = {
    let mut create_price = CreatePrice::new(Currency::USD);
    create_price.product = Some(IdOrCreate::Id(&product.id));
    create_price.metadata = Some(
        std::collections::HashMap::from(
             [(String::from("async-stripe"), String::from("true"))]
        )
    );
    create_price.unit_amount = Some(1000);
    create_price.expand = &["product"];
    Price::create(&client, create_price).await.unwrap()
};

Note that here, we have the currency as US dollars. The unit amount is the smallest possible currency for a given currency, meaning that the total amount for this price is actually $10.00 (or “1000 cents”), not $1000! As before with the product information itself, we can add whatever metadata we want to the price object. We are also required to input a product ID as the price object depends on a product ID being present.

You can additionally set things up like tiered pricing (based on unit volume!) programmatically. You can find more about the price object from this docs page here.

Subscriptions

Subscriptions are worth a special mention. Although they are technically just “prices with a recurring period” and you can use them just like a regular price object in Stripe, you may want to structure them differently. For example: How do you set your subscription tiers up properly?

To create a subscription price, when creating the price object you need to add the recurring property like so:

async fn create_product_price(
    client: &Client,
    product: &Product,
) -> Result<Price, ApiError> {
    let price = {
        let mut create_price = CreatePrice::new(Currency::USD);
        create_price.product = Some(IdOrCreate::Id(&product.id));
        create_price.metadata = Some(std::collections::HashMap::from([(
            String::from("async-stripe"),
            String::from("true"),
        )]));
        create_price.unit_amount = 1000;

        create_price.recurring = Some(CreatePriceRecurring {
            interval: CreatePriceRecurringInterval::Month,
            ..Default::default()
        });
        create_price.expand = &["product"];
        Price::create(client, create_price).await?
    };

    Ok(price)
}

Some things you may want to keep in mind for subscriptions:

  • Do you have multiple subscription tiers? If so, it may be a good idea to keep them as separate products
  • You can also have multiple prices for said tiers. This means you can set things up like having a monthly recurring bill and then offer a discount on a yearly recurring bill.

Cancelling a user subscription is fairly easy. To do so, you can use Subscription::cancel:

async fn cancel_subscription(subscription_id: String) -> Result<(), stripe::Error> {
		let _ = Subscription::cancel(
        &client,
        &SubscriptionId::from_str(subscription_id).unwrap(),
        CancelSubscription {
            cancellation_details: None,
            invoice_now: Some(true),
            prorate: Some(true),
        },
    )
    .await?;

    Ok(())
}

Note that here, we’ve additionally told Stripe we want to invoice now with prorating and no additional cancellation details. You can simply set it to None if you don’t want instant invoicing (or if you don’t want prorating).

Updating subscription tiers or the details of a subscription however, is somewhat more complicated. To do so, you need to retrieve a user’s subscription ID somewhere (this ID would ideally be stored in your database) and then adjust the relevant item in your subscription items list. See below for an example of a subscription with only one item on the subscription list (the base subscription price):

async fn update_subscription(
    user_subscription_id: String,
    old_item_id: String,
    new_item_id: String,
    new_price_id: String
) -> Result<(), stripe::Error> {
		let subscription_item = Subscription::retrieve(
        &client,
        &SubscriptionId::from_str(&user_subscription_id).unwrap(),
        &["items"],
    )
    .await?
    .items;

    let subscription_item = &subscription_item.data[0];

    let _ = Subscription::update(
        &client,
        &SubscriptionId::from_str(&user_subscription_id).unwrap(),
        UpdateSubscription {
            items: Some(vec![UpdateSubscriptionItems {
                id: Some(old_item_id),
                deleted: Some(true),
                ..Default::default()
            },
              UpdateSubscriptionItems {
                id: Some(new_item_id),
                price: Some(new_price_id)
                ..Default::default()
            },
						]),
            ..Default::default()
        },
    )
    .await?;

		Ok(())
}

As you can see, although somewhat complicated it is not too difficult. We simply delete the old item, then add a new one with the relevant price and item ID.

Creating Checkout Sessions

Now for the fun part: getting paid! We can create a checkout session with a customer ID, then add a checkout with a price ID. Then once the session has been created, we can send the URL back to the user!

Note that we’ve added a cancel and success URL to explicitly tell Stripe where we want to send our user to after; if this isn’t set, they will stay on the Stripe page.

async fn create_checkout_session(customer_id: String) -> String {
    let checkout_session = {
        let mut params = CreateCheckoutSession::new();
        params.cancel_url = Some("http://test.com/cancel");
        params.success_url = Some("http://test.com/success");
        params.customer = Some(customer_id);
        params.mode = Some(CheckoutSessionMode::Payment);
        params.line_items = Some(
            vec![CreateCheckoutSessionLineItems {
                quantity: Some(1),
                price: Some(price.id.to_string()),
                ..Default::default()
            }]
        );
        params.expand = &["line_items", "line_items.data.price.product"];

        CheckoutSession::create(&client, params).await.unwrap()
    };

    let line_items = checkout_session.line_items.unwrap();

    checkout_session.url.unwrap()
}

When we return the URL to the user (assuming they visit the URL), once the checkout is completed, they’ll now have purchased a product or service from us! If we have webhooks set up (see the later webhooks section), we will receive a CheckoutSessionCompleted event which will hold the user subscription ID that we can save and use in other scenarios (for example, updating/cancelling subscriptions). Because the ID by itself cannot be used for anything without an API key, it is not considered sensitive data and is therefore safe to store without much additional consideration.

Setting up customers

To set up a customer on Stripe, you can easily create one like so:

let secret_key = std::env::var("STRIPE_API_KEY").expect("Missing STRIPE_API_KEY in env");
let client = Client::new(secret_key);

let customer = Customer::create(&client, CreateCustomer {
    name: Some("Josh"),
    email: Some("test@async-stripe.com"),
    description: Some("A fake customer that is used to illustrate the examples in async-stripe."),
    metadata: Some(
        std::collections::HashMap::from([(String::from("async-stripe"), String::from("true"))])
    ),

    ..Default::default()
}).await.unwrap();

Here we have added a test customer to Stripe, with some metadata, an email and a description. You can find out more about the CreateCustomer struct here, which shows all of the fields/methods that can be used.

Note that while it is possible to additionally add a payment method for a customer through the API, you are required to be PCI compliant while using Stripe; processing or handling of card information of any kind through an API requires you to meet much more stringent criteria than if you just set up a Stripe checkout session and allow the user to input their details into the checkout session.

If you’re still interested in adding a payment method manually through your API, see below:

let payment_method = {
        let pm = PaymentMethod::create(
            &client,
            CreatePaymentMethod {
                type_: Some(PaymentMethodTypeFilter::Card),
                card: Some(CreatePaymentMethodCardUnion::CardDetailsParams(
                       CardDetailsParams {
                    number: "4242424242424242".to_string(), // test card number
                    exp_year: 2025,
                    exp_month: 1,
                    cvc: Some("123".to_string()),
                    ..Default::default()
                })),
                ..Default::default()
            },
        )
        .await
        .unwrap();

        PaymentMethod::attach(
            &client,
            &pm.id,
            // this customer ID is taken from earlier when creating the customer
            AttachPaymentMethod { customer: customer.id.clone() },
        )
        .await
        .unwrap();

        pm
    };

Webhooks

Stripe additionally offers webhooks that we can use to receive events! Whenever someone sets up a new subscription, updates or cancels one, we can have an event sent to a web service that we own!

While we can do this manually, async-stripe has examples that we can use in extractors. The way that we do this is by wrapping stripe::Event in a struct that then implements the relevant extractor trait, checking the stripe-signature header to ensure HTTP request integrity and construct the payload.

Below is a snippet of an Axum custom extractor that wraps stripe::Event - you can find the full example here.

struct StripeEvent(Event);

#[async_trait]
impl<S> FromRequest<S> for StripeEvent where String: FromRequest<S>, S: Send + Sync {
    type Rejection = Response;

    async fn from_request(req: Request<Body>, state: &S) -> Result<Self, Self::Rejection> {
        let signature = if let Some(sig) = req.headers().get("stripe-signature") {
            sig.to_owned()
        } else {
            return Err(StatusCode::BAD_REQUEST.into_response());
        };

        let payload = String::from_request(req, state).await.map_err(IntoResponse::into_response)?;

        Ok(
            Self(
                stripe::Webhook
                    ::construct_event(&payload, signature.to_str().unwrap(), "whsec_xxxxx")
                    .map_err(|_| StatusCode::BAD_REQUEST.into_response())?
            )
        )
    }
}

We can then use it in a function like this, where StripeEvent is passed in as a function argument (enabled by it implementing FromRequest):

async fn my_function(
	StripeEvent(event): StripeEvent,
) -> impl IntoResponse {
    match event.type_ {
    EventType::CheckoutSessionCompleted => {
        // .. do some stuff here
    }
    EventType::SubscriptionScheduleCanceled => {
       // .. do some stuff here
    }
    _ => {}
    }

    StatusCode::OK
}

Webhooks normally require HTTPS to test - if you’re trying to make this work locally, you can use Cloudflare Tunnel, Ngrok or a similar service to receive webhooks.

Interested in checking out the examples for Actix Web and Rocket? The async-stripe GitHub repo has those examples here.

Finishing up

Thanks for reading! Using Stripe in Rust web services is a great way to start being able to make money from Rust. By reading this guide to using Stripe with Rust, you should be one step closer to exactly that.

  • Learn how you can use Stripe with a full-stack Loco template here.
  • Learn about rate limiting your API here.
  • Have a look at getting started with Tracing logging libraries here.
This blog post is powered by shuttle - The Rust-native, open source, cloud development platform. If you have any questions, or want to provide feedback, join our Discord server!
Share article
rocket

Build the Future of Backend Development with us

Join the movement and help revolutionize the world of backend development. Together, we can create the future!