Proper Way to Verify Gamepass Purchase?

  1. What do you want to achieve?

I would like to know what’s the proper way to verify if a player successfully purchased a Gamepass.

  1. What solutions have you tried so far?

I searched the Dev Forum, youtube, etc. I believe I already did a lot of research… to no avail (currently).

  1. What is the issue?

In every post I see, they are all using the wasPurchased param from the PromptGamePassPurchaseFinished event. The Roblox documentation explicitly told us not to rely on it: “This might not accurately reflect if the purchase itself has been successfully processed.”

Then, in a few posts, they decide to use UserOwnsGamePassAsync, which is a little better than the other posts I mentioned above. Nevertheless, the Roblox documentation for UserOwnsGamePassAsync says this: “If the user has purchased the pass inside an experience through PromptGamePassPurchase, the UserOwnsGamePassAsync function might return false because of caching behavior.”

What am I supposed to use then? I believe there must be a way to do it without using a proxy.

For me, UserOwnsGamePassAsync works fine but if you’re actually worried about it then you could save the gamepasses they own/bought inside a datastore. Saving it inside a data store would avoid the caching issue. If they bought it for the first time, you could add UserOwnsGamePassAsync on top of the PromptGamePassPurchaseFinished event for extra safety. Because you want to prioritize reliability over convenience, I’d recommend a data manager module so other scripts that access the data

1 Like

Cool! Thanks for the reply!

So you’re saying that instead of using UserOwnsGamePassAsync when they join, I can check using a DataStore, and that I only use the UserOwnsGamePassAsync for verifying when they buy it for the first time (and then store it to the DataStore if they bought it). That way, I can dodge the caching behavior of UserOwnsGamePassAsync?

Yes. Unless some other person comes up with a better idea or disapproves of my idea then it works

1 Like

I’m not sure if that’s worth the effort. As you’ve stated, the documentation contradicts the reliability of either method, so if @sonic_848 says UserOwnsGamePassAsync has yet to fail them, know that wasPurchased has yet to fail me. I am not aware of any resource that elaborates on the edge cases in which these warnings come true. I designed my system to lean towards UserOwnsGamePassAsync being unreliable

1 Like

Thanks for your reply!

I checked out your module (great job on the clean code and documentation, I rarely see devs like this!) and you seem to trust the wasPurchased and even trust it more than UserOwnsGamePassAsync. Any explanations, any example of live games you used with this module, and any thoughts on why Roblox would write that if it’s actually not that unrealiable?

Considering PromptGamePassPurchaseFinished does not fire until the transaction menu has been cleared, it’s hard to imagine why wasPurchased would be marked unreliable. You’d assume Roblox would only allow the boolean to be true upon receiving an HTTP 200 status code from their API endpoint. It could be that some catastrophic error could occur within their monetization pipeline that is disconnected from said API endpoint, which could lead to the transaction being cancelled or corrupted on Roblox’s end. It is easier to believe UserOwnsGamePassAsync would be unreliable due to a given reason: caching behaviour. What Roblox means specifically is that prior ownership confirmation requests that come back false could be cached, where future requests after purchase could dig up the false cache. This is why I decided to develop my own dynamic cached based on wasPurchased, as UserOwnsGamePassAsync’s flaws are controllable.

I have used my system in projects before, and it’s served me and my coworkers very well. It streamlines implementing developer products and game-passes, while providing niceties like instant good/service activation upon purchase and objective control over in-game monetization

1 Like

I went through your module again, and I have one more question: How do you check if a player owns a gamepass when they just joined? I see you have a cache and use setGamePassOwned for actions when they buy a gamepass, studio ownership, unofficial ownership, etc., but again, How do you check if a player owns a gamepass when they just joined?

@sonic_848 mentioned the use of datastores, and correct me if I’m wrong, but I think you don’t recommend that. What do you recommend then?

The module in question is a service component of a feature in the Folder-per-Feature architecture. Services are meant to provide essential tools, while the overall functionality of the feature is scripted using those tools. This is what enables flexibility in the feature. I use the tryLoadGamePassAsync function for all current and future players and game-passes on server startup:

--!strict
local Players = game:GetService("Players")


local Feature = script.Parent
local Service = require(Feature.Service)
local Types   = require(Feature.Types)



local function onPlayerAdded(player: Player)
	for _, gamePass in Service.getGamePasses() do
		Service.tryLoadGamePassAsync(player, gamePass)
	end
end



for _, player in Players:GetPlayers() do
	task.spawn(onPlayerAdded, player)
end

Players.PlayerAdded:Connect(onPlayerAdded)
Service.GamePassRegistered:Connect(Service.tryLoadGamePassForAllAsync)

A data-store can’t function as a solution since you’d be relying on wasPurchased or UserOwnsGamePassAsync to verify the transaction’s success. We’ve already weighed the uncertainty of both methods, and it seems that wasPurchase is inductively more reliable. This means you need only solve the local caching issues with UserOwnsGamePassAsync, which does not require a data-store. We can continue to let Roblox handle the recording of game-pass ownership

1 Like

Wow! Thanks for your explanation. I looked at your source for tryLoadGamePassAsync and it eventually uses:

local owned = ownsGamePassAsync(userId, gamePassId)

and the source for that is

local function ownsGamePassAsync(userId: number, gamePassId: number): boolean
	return ownsGamePassInCache(userId, gamePassId) 
		or ownsGamePassInStudio(userId, gamePassId) 
		or MarketplaceService:UserOwnsGamePassAsync(userId, gamePassId) 
		or ownsGamePassUnofficiallyAsync(userId, gamePassId)
end

So based on your implementation of the Players.PlayerAdded:Connect(onPlayerAdded), the line that would run would be MarketplaceService:UserOwnsGamePassAsync(userId, gamePassId) assuming the cache found no results, correct?

Yep! That is precisely what happens. However, in my system, I added an objective control flag called “OwnsGamePassesInStudio”. Enabling this BoolValue within the feature’s configuration allow you to own any current and future registered game-pass. It’s a super helpful testing tool. This check is made before querying Roblox. To think of it, I should even make this check before checking the cache

1 Like

Alrighty, thanks so much for all the help! @sonic_848 thanks too!!

1 Like

The system also comes built with developer product and game-pass gifting support. So even if the flag is not raised, no positive cache exists, and Roblox doesn’t acknowledge ownership, the user still could’ve been gifted the game-pass offline or online. This would be the final stage in understanding whether or not a user owns a game-pass

1 Like