Skip to main content
Solved

Creating SKUs and IPAs for market place


Forum|alt.badge.img+1

Hello!

I am posting here in order to double-check my approach when it comes to creating a market-place and allow sellers to pick an arbitrary price for selling digital products.

As such, and that’s as far as my planning has brought me, I came up with a solution where I’d create SKUs and IPAs which represent a particular price-point like:

storeIdentifier  |  price   |
-----------------|----------|
item_0050        |  €  0.50 |
item_0100        |  €  1.00 |
item_0150        |  €  1.50 |
...              |          |
item_9999        |  € 99.99 |

I do this, because it appears not to be possible to simply define a `store_product` item with a base price of €0.01 (one Euro cent) and then control it using quantity such that users would buy 1000 units of `store_product` to be charged €10.00.

As I am working on fully-separate environments, I’d have to replicate these SKUs and IPAs in all environments and mirror all of that in RevenueCat. That’s not the issue of course per-se, but I want to make sure that this approach is the best I can chose for this type of requirements.

Any advice would be much appreciated!

Best answer by joan-cardona

Hi ​@s-falk,

Unfortunately that’s a very common issue. Apple has started to address it creating the Advanced Commerce API that makes the customer define the price of the purchase but you have to request access to it and they don’t grant it easily.

The issue with creating one product and add quantity is that the user would have to purchase 1000 units (and confirm those 1000 purchases!) which it adds a lot of friction.

I believe your current approach of creating different products is the best one even though it still comes with a lot of downsides. It still depends on how many products you plan to create but managing them all is not easy. The stores also have a limit on the number of products you can create and you’ll have to get them all reviewed (and visible) as well. 

TLDR; you are doing the best approach but it comes with downsides that you have to consider if they are worth it.

Best,

View original
Did this post help you find an answer to your question?

7 replies

joan-cardona
RevenueCat Staff
Forum|alt.badge.img+6
  • RevenueCat Staff
  • 493 replies
  • Answer
  • June 26, 2025

Hi ​@s-falk,

Unfortunately that’s a very common issue. Apple has started to address it creating the Advanced Commerce API that makes the customer define the price of the purchase but you have to request access to it and they don’t grant it easily.

The issue with creating one product and add quantity is that the user would have to purchase 1000 units (and confirm those 1000 purchases!) which it adds a lot of friction.

I believe your current approach of creating different products is the best one even though it still comes with a lot of downsides. It still depends on how many products you plan to create but managing them all is not easy. The stores also have a limit on the number of products you can create and you’ll have to get them all reviewed (and visible) as well. 

TLDR; you are doing the best approach but it comes with downsides that you have to consider if they are worth it.

Best,


Forum|alt.badge.img+1
  • Author
  • Member
  • 8 replies
  • June 28, 2025

Thanks a lot for the confirmation!

At the moment I plan to create 200 to 300 such price-points. From €1 to €20 (maybe €30) in €0.1 step-size which should cover all my needs. 

I hope that Apple won’t have too much difficulties there reviewing them. They’ll be created using automation which will make them quite predictive/repetitive.


joan-cardona
RevenueCat Staff
Forum|alt.badge.img+6
  • RevenueCat Staff
  • 493 replies
  • July 9, 2025

Hi ​@s-falk,

Sounds good, we have seen customers with that many IAP and it shouldn’t be a problem.

Let me know if I can help!


Forum|alt.badge.img+1
  • Author
  • Member
  • 8 replies
  • July 9, 2025

@joan-cardona Hey again! :) 

 

I am currently struggling a bit with how I should design the purchase flow.

The problem: If I use price_0150 to charge € 1.50, how can I make sure that I can verify this on my backend?

With the following in my React Native app:

/**
 * @param item The item being purchased.
 * @param pricePoint The price-point to charge for this item.
 */
const handlePurchase = async (item: any, pricePoint: any) => {
  if (!user) {
    return;
  }
  try {
    await Purchases.logIn(user?.id);
    // Fetch the product using internal price-point ID
    const products = await Purchases.getProducts([pricePoint.id]);
    const product = products[0];
    if (!product) {
      // Not found
      return;
    }
    const purchaserInfo = await Purchases.purchaseStoreProduct(product);
  } catch (error) {
    console.error(error);
  }
};

The webhook-event won’t contain the item.id which means I won’t be able to match the transaction ID on my backend. 

A second issue with this is that, theoretically, a client could send a different pricePoint.id and get charged wrongly.

This is why I might have to create a “pending” purchase on my backend first and verify it in a final step like so:

// Creat the purchase (status will be "pending")
const itemPurchase = await itemPurchaseApi.createItemPurchase({
  itemId: item.id,
  pricePointId: pricePoint.id,
});

// Now make the purchase
const purchaserInfo = await Purchases.purchaseStoreProduct(product);

// Verify purchase
await itemPurchaseApi.verifyItemPurchase({
  purchaseId: itemPurchase.id,
  transactionId: purchaserInfo.transaction.transactionIdentifier,
});

With this, I think, I would eliminate the need for a webhook but I am not sure if I could (or should) do it like this. Furthermore, if anything goes wrong, the purchase might not be verifiable so this feels a bit brittle.

Imo some issues could be avoided if I was able to inject data for the webhook event:

const purchaserInfo = await Purchases.purchaseStoreProduct(product, {
  additionalEventData: { itemId: item.id },
});

If the event contained my internal event-id, then I could do the verification on my backend by comparing without needing this extra call.

 

Am I over-complicating things here? 


joan-cardona
RevenueCat Staff
Forum|alt.badge.img+6
  • RevenueCat Staff
  • 493 replies
  • July 15, 2025

Hi ​@s-falk,

The webhook data should include the product id in the `product_id` field. This id should match the one in RevenueCat so you should be able to always know which item has been purchased.

You can see a webhook example here.

Is that id what you are looking for?

 

Best,


Forum|alt.badge.img+1
  • Author
  • Member
  • 8 replies
  • July 23, 2025

@joan-cardona Hello! :) 

 

Unfortunately, that’s not possible in my case. I have a market place where users can set arbitrary prices for their items. The price itself is the product_id but that’s a one-to-many relation to items. Since I cannot pass on the item_id, the item a user sells, I cannot (reliably) match the webhook event to a specific item being purchased. Come to think of it: It’s likely that this isn’t a problem that can be solved by RevenueCat because there’s always a way to spoof the product_id unless I move authorization to my own backend.

But just in case I overlook something, here is a deeper explanation of what’s happening. 

This is how my table looks like. Users (ownerId) can create offers for items (itemId) and set arbitrary prices (product_id - the RevenueCat internal ID)

 

// table: offers

| ownerId | itemId  |  product_id    |  comment  |
|---------|---------|----------------|-----------|
| 1       | aaa     |  price_0199    |  $  1.99  |
| 2       | bbb     |  price_0199    |  $  1.99  |
| 3       | ccc     |  price_1299    |  $ 12.99  |
| 4       | ddd     |  price_1399    |  $ 13.99  |

So.. in this example Item aaa and bbb share the product_id.

What this means for my flow as a user clicks “buy item”:

In order to identify everything correctly, I need to get my hands on the transaction_id, store it on my backend and wait for the RevenueCat event with the same transaction_id. In more detail:

  1. I need to create a item_purchase on my backend with status PENDING
    1. My backend will look-up the product_id and remember it to prevent spoofing
  2. Call Purchases.purchaseStoreProduct
    1. User gets charged
    2. RevenueCat triggers a webhook which will fail initially on my backend
    3. I receive the transaction_id
  3. I have to update the item_purchase created in step 1 and store the transaction_id from step 2
    1. RevenueCat will retry my webhook
    2. This time I can identify the item_purchase using the transaction_id and verify the purchase and set the status to VERIFIED

Client-Side Code Example

const purchaseTour = async (
  tourPurchaseApi: TourMobilePurchaseApi,
  user: User,
  tourPreview: TourPreview,
) => {
  console.info('Attempting to purchase tour ..');
  if (!tourPreview.pricing?.revenueCatPrice) {
    console.debug('No pricing available for this tour');
    return;
  }

  console.debug('handleOnPay');

  const { productId } = tourPreview.pricing.revenueCatPrice;

  console.debug(`RevenueCat login user ${user.id}`);
  await Purchases.logIn(user.id);

  console.debug('Fetching RevenueCat product', productId);

  const products = await Purchases.getProducts(
    [productId],
    // Override default type (crucial for Android)
    PRODUCT_CATEGORY.NON_SUBSCRIPTION,
  );
  const product = products[0];

  if (!product) {
    // Not found
    console.error(`Product not found ${productId}`);
    const error = new Error(`Product not found (productId ${productId})`);
    Sentry.captureException(error);
    throw error;
  }

  console.debug('Received:', JSON.stringify(products));

  try {
    /*
     * Creates a mobile purchase. The server will internally fetch and store the productId and compare it later
     * during verification. Should product IDs mismatch, the purchase won't be verified.
     */
    console.debug('Create TourMobilePurchase on backend');
    const mobilePurchase = await tourPurchaseApi.createTourMobilePurchase({
      createTourMobilePurchase: {
        tourId: tourPreview.id,
      },
    });

    console.debug('Purchasing store product', product.identifier);
    const purchaserInfo = await Purchases.purchaseStoreProduct(product);

    /*
     * Required action: The transaction ID here will be compared to the RevenueCat webhook event
     * transaction ID to verify the purchase.
     */
    console.debug(
      'Setting transactionId store product',
      purchaserInfo.transaction.transactionIdentifier,
    );
    await tourPurchaseApi.setTourMobilePurchaseTransactionId({
      id: mobilePurchase.id,
      transactionId: purchaserInfo.transaction.transactionIdentifier,
    });

    console.info('Tour purchase success', purchaserInfo);
  } catch (error) {
    Sentry.captureException(error);
    console.error('Tour purchase error', error);
    throw error;
  }
};

 


joan-cardona
RevenueCat Staff
Forum|alt.badge.img+6
  • RevenueCat Staff
  • 493 replies
  • August 5, 2025

Hi ​@s-falk,

Thank you for the detailed post! Let me dig into it since this is not an easy problem and I can find if any way we can make it work better.

Best,


Reply


Cookie policy

We use cookies to enhance and personalize your experience. If you accept you agree to our full cookie policy. Learn more about our cookies.

 
Cookie settings