Skip to main content
Solved

Non-Sub Consumable Metadata

  • May 12, 2026
  • 3 replies
  • 83 views

jamessherlock
Forum|alt.badge.img

Hey! I’m exploring a system in my app where users can either pay a one-time fee to unlock a single  specific article in a news app, or subscribe (monthly) to unlock all articles in the app.

The subscription is easy. Product => Entitlement, check for the entitlement, badaboom, done.

I am however not 100% sure on the best way to “unlock a single article”.

I understand how to trigger the purchase, and I understand I need to implement a webhook against NON_RENEWING_PURCHASE to confirm receipt. What I’ve yet to nail down is the method in which my server knows which article to unlock.

  1. Is there a way to attach any form of metadata in the purchase? (`article: 123456`). I can’t seem to spot it.
  1. Alternatively, is the flow to have the app itself (after the purchase goes through) call an “unlock” API on my server with transactionId and article ID, which then looks for a corresponding webhook?

With this flow, how would folks recommend handling the race condition between the webhook (which per docs can take in excess of 60s, though often much quicker) vs the purchase confirmation (which is very quick).

  1. Is the third option to skip the webhook entirely and use the REST API? iOS SDK confirms purchase, calls my API with the transactionId and article ID, my API queries RevenueCat to confirm purchase, then unlocks the content? (Will RevenueCat always have the transaction before purchase callback is called?) 

Any and all insights are very much appreciated! Thanks

 

(Note: I have for the purpose of discussion simplified the actual product, it’s not articles… but it’s easier to discuss when we all understand the proposition!)

Best answer by hussain

 Hi ​@jamessherlock,

Thanks for reaching out. I’m happy to help.

As of today there isn't a generally available way to attach arbitrary metadata to an iOS in-app purchase that gets propagated through to the NON_RENEWING_PURCHASE webhook today. So options 2 and 3 are where you want to focus. I'd push you toward your third option (REST API verification) as the primary path, with the webhook as a reconciliation safety net. 

 

Here's how I'd structure it:

  1. User taps "Unlock article 123456" -> SDK kicks off the StoreKit purchase.
  2. When the SDK's purchase completion fires successfully, the app POSTs { app_user_id, article_id, store_transaction_id } to your backend.
  3. Your backend calls GET /v1/subscribers/{app_user_id} and looks for that store_transaction_id (or the corresponding RevenueCat-assigned id) inside the non_subscriptions map for the right product identifier.
  4. If it's there, persist (article_id, transaction_id, user_id) in your DB and grant access. If not, return a soft error and let the app retry with backoff.

Some things to keep in mind:

  • Refunds: listen for CANCELLATION/refund events and revoke article access in your DB. This is the big one.
  • Server-driven reconciliation: a nightly job that confirms every unlock has a corresponding RC transaction.

Hope this helps, let me know if you have any other questions.

Best,

Hussain

3 replies

hussain
RevenueCat Staff
Forum|alt.badge.img+6
  • RevenueCat Staff
  • Answer
  • May 14, 2026

 Hi ​@jamessherlock,

Thanks for reaching out. I’m happy to help.

As of today there isn't a generally available way to attach arbitrary metadata to an iOS in-app purchase that gets propagated through to the NON_RENEWING_PURCHASE webhook today. So options 2 and 3 are where you want to focus. I'd push you toward your third option (REST API verification) as the primary path, with the webhook as a reconciliation safety net. 

 

Here's how I'd structure it:

  1. User taps "Unlock article 123456" -> SDK kicks off the StoreKit purchase.
  2. When the SDK's purchase completion fires successfully, the app POSTs { app_user_id, article_id, store_transaction_id } to your backend.
  3. Your backend calls GET /v1/subscribers/{app_user_id} and looks for that store_transaction_id (or the corresponding RevenueCat-assigned id) inside the non_subscriptions map for the right product identifier.
  4. If it's there, persist (article_id, transaction_id, user_id) in your DB and grant access. If not, return a soft error and let the app retry with backoff.

Some things to keep in mind:

  • Refunds: listen for CANCELLATION/refund events and revoke article access in your DB. This is the big one.
  • Server-driven reconciliation: a nightly job that confirms every unlock has a corresponding RC transaction.

Hope this helps, let me know if you have any other questions.

Best,

Hussain


jamessherlock
Forum|alt.badge.img
  • Author
  • New Member
  • May 14, 2026

Awesome stuff, thanks for the direction ​@hussain - will go with that plan!

On step 4, what are the odds that the API would not have the transaction after the purchase closure is returned as successful on-device? Is this a rarity (<1%) or fairly common?


hussain
RevenueCat Staff
Forum|alt.badge.img+6
  • RevenueCat Staff
  • May 17, 2026

Hey ​@jamessherlock ,

I’m glad I was able to help.

Now coming back to your question. In practice it's rare. The iOS SDK's purchase completion only fires after the receipt has been posted to our backend and verified with Apple, so by the time your app sees success, the transaction is almost always already on file with us.

The cases where it isn't usually come down to brief cache propagation, or a flaky network delaying the receipt post.

So I'd build step 4 to be forgiving rather than assume strict consistency:

  • If the store_transaction_id isn't in non_subscriptions on the first call, retry with exponential backoff (1s, 2s, 4s. 3 attempts is plenty).
  • If it still isn't there, fall back to the webhook as the reconciliation path.

That combo covers essentially every case without making the user wait on webhook latency.

Hope this helps, let me know if you have any other questions.

Best,

Hussain