It is important for developers to optimize third-party API usage to reduce costs, improve performance, and enhance user experience.
An effective strategy to minimize the number of API calls to Stripe while still obtaining the necessary data involves caching and using the response Expand feature. This article discusses how using the expanding feature helps you retrieve related objects in a single request and tradeoffs of this approach. It also explores the use of caching for additional performance improvements, as well as show you how to inspect requests using tools available in Stripe Workbench.
The code samples in this post use the C# and Stripe .NET SDK, but the concepts are applicable for all supported languages.
Expand resource requests
All core resources in the Stripe API, such as Prices and Products, have unique ID properties used to interact with specific instances. These IDs link related resources, like associating a price with its product or a customer with a subscription. When you retrieve an instance via an API request, you receive a baseline set of properties.
For example, when an e-commerce application must display detailed information about a product, it retrieves the product instance using its ID. This includes essential data like the product name, description, and images, which are necessary for providing customers with comprehensive product details on the website.
Using the ProductService
class, pass the ID of a product in your Stripe catalog to the GetAsync
method.
var requestOptions = new RequestOptions { ApiKey = "<secret-key>"}; var productService = new ProductService(); var product = await productService.GetAsync("<product-id>", requestOptions: requestOptions);
The API response returns a JSON payload that all Stripe SDKs deserialize for you automatically. A typical payload resembles the example below.
{ "id": "prod_NWjs8kKbJWmuuc", "object": "product", "active": true, "created": 1678833149, "default_price": "<price-id>", "description": "Black, Long Sleeve, Vintage Horizon Shirt", "images": [], "metadata": {}, "name": "Vintage Horizon Shirt", "tax_code": null, "updated": 1678833149, "url": null }
This sample response shows a reduced set of returned properties.
Depending on what your goal is, this information may be sufficient. Notice the response does not contain any detailed pricing information. If you do need a price for this product, the only thing available for you to work with is the ID returned in the default_price
property. You can use that property to issue a second request to retrieve the pricing information.
var priceService = new PriceService(); var price = await priceService.GetAsync(product.DefaultPriceId, requestOptions: requestOptions);
This is where it becomes important to understand what your applications are trying to accomplish. In some cases, it might be sufficient to not include that additional price data if it’s not needed. Returning less data results in faster response times and less of a payload to process, especially in high traffic scenarios. If your application needs the price along with the other product data, then it makes sense to retrieve them both at the same time if you can.
You can return related data, like the default_price
property of a product, by using the expand parameter in API requests to Stripe. The API reference docs for the Product object highlights which properties are expandable.
To replace those two previous requests with a single one that uses the expand parameter, use the ProductGetOptions class from the .NET SDK and provide it with a list of the expandable properties you need. For now it is just the default_price, but you can add additional properties to the list.
var getOptions = new ProductGetOptions { Expand = new List<string> { "default_price" } }; var productService = new ProductService(); var product = await productService.GetAsync("prod_PnyyMavtgHZNqV", getOptions, requestOptions: requestOptions);
That API response from the ProductService
now contains more information. The default_price
property is no longer a string but a nested object containing details about the associated price.
{ "id": "prod_NWjs8kKbJWmuuc", "object": "product", "active": true, "created": 1678833149, "default_price": { "id": "price_1OyN06BY4YhJumpKWZaLyKnJ", "object": "price", "active": true, "billing_scheme": "per_unit", "created": 1711409542, "currency": "cad", "product": "prod_PnyyMavtgHZNqV", "recurring": null, "tax_behavior": "unspecified", "type": "one_time", "unit_amount": 4000, "unit_amount_decimal": "4000" }, "description": "Black, Long Sleeve, Vintage Horizon Shirt", "images": [], "metadata": {}, "name": "Vintage Horizon Shirt", "tax_code": null, "updated": 1678833149, "url": null }
This sample response shows a reduced set of returned properties.
You can use the expand feature in Stripe with Product, Price, and many other resources. This feature allows you to expand multiple properties on the same resource, including nested properties up to four levels deep. For example, if you need to get the currency options for a product's default price, use the dot notation in the expand parameter like this: default_price.currency_options
.
{ "id": "prod_PnyyMavtgHZNqV", "object": "product", "created": 1711409542, "default_price": { "id": "price_1OyN06BY4YhJumpKWZaLyKnJ", "object": "price", "billing_scheme": "per_unit", "created": 1711409542, "currency": "cad", "currency_options": { "cad": { "custom_unit_amount": null, "tax_behavior": "unspecified", "unit_amount": 4000, "unit_amount_decimal": "4000" } }, "product": "prod_PnyyMavtgHZNqV", "recurring": null, "tax_behavior": "unspecified", "type": "one_time", "unit_amount": 4000, "unit_amount_decimal": "4000" }, "description": "Black, Long Sleeve, Vintage Horizon Shirt", "livemode": false, "metadata": {}, "name": "Vintage Horizon Shirt", "tax_code": "txcd_30011000" }
This sample response shows a reduced set of returned properties.
After issuing that request, you have the available currency options for the price of the specified product.
Working with resource lists
The expand feature is also available when working with lists of resources. Imagine your application contains a product listing page where it needs to show the name, image, description and price for each item. You must make an additional request to get the pricing information.
The ProductService
class contains a ListAsync
method that you can use to return products that match the supplied list options.
var listOptions = new ProductListOptions(){ Active = true, Limit = 10 }; var productService = new ProductService(); var priceService = new PriceService(); await foreach (var product in productService.ListAutoPagingAsync(listOptions, requestOptions)) { var price = await priceService.GetAsync(product.DefaultPriceId, requestOptions: requestOptions); // add product data to pricing page }
In the Stripe Dashboard, you can view the number of requests made to retrieve product information in the Logs tab in Workbench.
The initial request for the list products is accompanied by six additional requests to get the price information.
If you inspect the payload for responses that return lists, you can see items inside of an array property named data.
{ "object": "list", "url": "/v1/products", "has_more": false, "data": [ { "id": "prod_NWjs8kKbJWmuuc", "object": "product", "active": true, "created": 1678833149, "default_price": null, "description": null, "images": [], "livemode": false, "metadata": {}, "name": "Gold Plan", "updated": 1678833149, "url": null } ] }
You can use the expand feature when working with lists of resources similar to requesting individual ones. To request expandable properties to be included in list request, prefix each property name with data
. This gives you a way to drill into the response and specify which property you need.
var listOptions = new ProductListOptions() { Active = true, Limit = 10, Expand = new List<string> { "data.default_price" } }; var productService = new ProductService(); await foreach (var product in productService.ListAutoPagingAsync(listOptions, requestOptions)) { var price = product.DefaultPrice; }
Back on the Logs tab in Workbench, refresh the logs and look at how many requests were made.
To get all the information to display six products on a listing page, it only required one request to the Stripe API.
It is important to note that using the expand feature does come with some performance cost, especially when working with nested expansions. Deeper expansions increase the response payload size leading to longer response times and higher resource consumption. It is recommended that nested expansions are used sparingly. Using techniques like caching can help you find a balance getting the information you need without needing to make frequent requests to the API.
Adding caching
In high traffic scenarios, you can make your API read requests more efficient by adding a caching layer. This approach can significantly reduce the number of requests your application makes to Stripe, helping improve performance while also avoiding hitting rate limits.
For instance, product information tends to remain static, meaning subsequent calls often return identical data. By temporarily storing this data in an in-memory cache with sensible expiration defaults, your application can access the needed information more rapidly and with fewer network hops, leading to a smoother and more responsive user experience.
// Create a cache instance somewhere accessible throughout your application var cache = new FusionCache(new FusionCacheOptions { DefaultEntryOptions = new() { Duration = TimeSpan.FromMinutes(5) } }); // User the cache to store product data from Stripe var listOptions = new ProductListOptions() { Active = true, Limit = 10, Expand = new List<string> { "data.default_price" } }; var productService = new ProductService(); // a for loop that iterates 10 times for (int i = 0; i < 10; i++) { var products = await cache.GetOrSetAsync("products", async _ => await productService.ListAsync(listOptions, requestOptions)); }
The previous code sample uses the in-memory cache capabilities of the FusionCache .NET library that allow you to configure various settings like the duration for cache entries. The GetOrSetAsync
method accepts a cache key and a factory method that knows how to get the data. Using this method has the added benefit of having FusionCache protect you against issues like cache stampede.
With these changes, your application can reduce API requests to Stripe while improving response times. With the original code sample, the number of requests to view a product listing page for one user would be 11: one request to get the list and 10 more to get the prices for each. For 1,000 concurrent users, that equates to 11,000 requests to Stripe. Using the caching example, it results in one request to Stripe with expansion to get the product and price data. All the other user requests would be served from the cache until it expires.
Conclusion
Understanding your application's data requirements and operational boundaries is crucial for optimal performance. While occasional requests for small data sets might be manageable, scaling your solution and handling more complex data necessitates additional considerations.
Tools like Workbench within your Stripe merchant account offer valuable insights into the frequency and nature of your requests. With this knowledge, you can strategically implement request expansion and caching techniques to enhance efficiency and scalability.