Retrieve all products with nested prices from stripe?

Viewed 3425

I enjoy the simplicity of the stripe APIs but I'm not sure if it's possible to retrieve nested objects. I'd love to be able to retrieve all products and prices to display them publically on my website.

I can list all prices and products like this:

stripe.Price.list(limit=3)
stripe.Products.list(limit=3)

On the frontend, I then have to write some logic that associates the prices with the products, makes the distinctions for monthly and yearly prices etc. I'd love to have this nested. Is this possible?

I'm also not entirely sure if it is safe to expose the returned information publically (API key is obviously hidden). I'd love some more info on this.

4 Answers

Whenever an id is present in an object, the expand parameter can be called to also return that item.

This can be further explored in the Stripe docs for Expanding Responses: https://stripe.com/docs/expand

A solution in nodejs would be the following:

const productPriceData = await stripe.prices.list({
  expand: ['data.product'], //  Give me the product data too!
})

I actually came across the same issue and documented how I got to this solution here: https://blog.focusotter.com/fetch-both-price-and-product-data-from-stripe-in-a-single-api-call

On the frontend, I then have to write some logic that associates the prices with the products, makes the distinctions for monthly and yearly prices etc. I'd love to have this nested. Is this possible?

The approach would be to first list the Products from the API, then iterate over them and retrieve all the Prices associated with a given Product:

The Product object doesn't contain a list of its Prices, so there is really no way around making the two separate requests: https://stripe.com/docs/api/products/object

I'm also not entirely sure if it is safe to expose the returned information publically (API key is obviously hidden). I'd love some more info on this.

This is a good question, since generally speaking only objects retrievable with a publishable key are safe to directly consume client-side from the Stripe API. In those cases the API reference marks the properties on the object as "retrievable with a publishable key", for example:

But, as long as your server-side endpoint doesn't directly return the Product/Price objects retrieved using your secret key, and instead returns a filtered version with just the required properties you should be fine!

Do it the other way around; get all the prices, then get the corresponding product.

Create your Stripe instance first, and then request the data from your function

// index.ts
import { getStripeProduct, getStripeProducts } from './products'
import * as express from 'express'
import Stripe from 'stripe'
const router = express.Router()
const stripe = (apiKey:string) => {
  return new Stripe(apiKey, {
    apiVersion: "2020-08-27",
    typescript: true
  });
}

// GET a product 
router.get('/product/:id', async (req: express.Request, res: express.Response) => {
  console.log(`API call to GET /public/product/${req.params.id}`)
  await getStripeProduct(stripe(STRIPE_SECRET_KEY)), req.params.id )
  .then((product) => {
    respond(res, {data: product})
  })
  .catch((error) => {
    respond(res, {code: error.statusCode, message: error.message})
  })
})

// GET a product list
router.get('/products', async (req: express.Request, res: express.Response) => {
  console.log('API call to GET /public/products')
  await getStripeProducts(stripe(STRIPE_SECRET_KEY))
  .then((products) => {
    respond(res, {data: products})
  })
  .catch((error) => {
    console.error(error)
    respond(res, {code: error.statusCode, message: error.message})
  })
})
// products.ts
import { ProductModel } from '../../models'
import { Stripe } from 'stripe'

export async function getStripeProduct(stripe: Stripe, id: string) {
  return new Promise((resolve, reject) => {
    stripe.prices.retrieve(id)
    .then(async (price) => {
      const product: ProductModel = {
        id: price.id,
        name: 'Unnamed Product',
        price: price.unit_amount,
        recurring: price.recurring || undefined,
        metadata: price.metadata
      }
        if(typeof(price.product) == 'string') {
        await stripe.products.retrieve(price.product)
        .then((stripeProduct) => {
          product.name = stripeProduct.name
          product.caption = stripeProduct.description || undefined
          product.images = stripeProduct.images || undefined
          product.active = stripeProduct.active
          resolve(product)
        })
      }
      resolve(product)
    })
    .catch((error:any) => {
      reject(error)
    })
  })
}

export async function getStripeProducts(stripe: Stripe) {
  return new Promise((resolve, reject) => {
    stripe.prices.list()
    .then(async (prices) => {
      const products: ProductModel[] = []
      for(let price of prices.data) {
        const product: ProductModel = {
          id: price.id,
          name: 'Unnamed Product',
          price: price.unit_amount,
          recurring: price.recurring || undefined,
          metadata: price.metadata
        }
        if(typeof price.product == 'string') {
          await stripe.products.retrieve(price.product)
          .then((stripeProduct) => {
            product.name = stripeProduct.name
            product.caption = stripeProduct.description || undefined
            product.images = stripeProduct.images || undefined
            product.active = stripeProduct.active
            products.push(product)
          })
        }
      }
      resolve(products)
    })
    .catch((error:any) => {
      reject(error)
    })
  })
}
// Product Model
export interface ProductModel {
  id: string;
  name: string;
  caption?: string;
  description?: string;
  active?: boolean;
  images?: string[];
  price?: string | number | null;
  recurring?: {
    interval: string,
    interval_count: number
  },
  metadata?: object
}

export class ProductModel implements ProductModel {
  constructor(productData?: ProductModel) {
    if (productData) {
      Object.assign(this, productData);
    }
  }
}

NB: I have a utility function called respond that standarizes our API responses. You can just res.send the response

Read ttmarek's answer first. However, note that the pages do not seem to provide a full example if you need all the answers. From "List all Prices":

Each entry in the array is a separate price object. If no more prices are available, the resulting array will be empty.

As it states, and is also stated here and in the docs, to get all of them, you need to loop until the resulting array is empty....

Here is some code that will get you all the products and all the prices...

import stripe
stripe.api_key = stripe_test_private_key
def get_all_stripe_objects(stripe_listable):
    objects = []
    get_more = True
    starting_after = None
    while get_more:
        #stripe.Customer implements ListableAPIResource(APIResource):
        resp = stripe_listable.list(limit=100,starting_after=starting_after)
        objects.extend(resp['data'])
        get_more = resp['has_more']
        if len(resp['data'])>0:
            starting_after = resp['data'][-1]['id']
    return objects
all_stripe_products = get_all_stripe_objects(stripe.Product)
all_stripe_prices = get_all_stripe_objects(stripe.Price)
Related