How to grant Roblox subscription perks correctly
Subscription perks saved forever, checked once or trusted from the client. How to check GetUserSubscriptionStatusAsync on the server and keep perks in step.
What it looks like
The game sells an experience subscription, but the perk doesn't track whether the player is subscribed right now. The status is checked once and never again, kept forever in saved data, or taken from the client.
Luau
if MarketplaceService:GetUserSubscriptionStatusAsync(player, subscriptionId).IsSubscribed then profile.Vip = true end
How to detect it
- Find every
GetUserSubscriptionStatusAsynccall and every perk tied to a subscription id (they look like"EXP-..."). - Roblox's documentation says the status check must be done on the server. If the server learns about the subscription from the client, through a remote argument or attribute the client sets, that's a critical problem. Report it at critical and say why.
- Check the call is inside
pcall. It's a web call and can fail. - Look for a handler on
Players.UserSubscriptionStatusChangedthat checks the status again. The server fires it after the subscription purchase prompt closes. Without a handler, a player who subscribes in-game gets nothing until they rejoin. - Check that the perk is taken away when
IsSubscribedisfalse. A flag saved to the profile when the player subscribes, and never cleared, gives the perk forever after one payment. - Monthly rewards are fine to save, such as a bundle of coins for each period paid for. The ongoing perk, such as VIP access or a multiplier, has to follow the current status.
What an exploiter could do
If the client can claim the subscription, anyone gets the perks without paying. If the perk is saved forever, a player can subscribe once, cancel, and keep it. Neither needs much effort, and both cost the game the recurring income the subscription was for.
How to fix it
- Check the status on the server with
GetUserSubscriptionStatusAsyncwhen the player joins, insidepcall, with a retry. - Connect
Players.UserSubscriptionStatusChangedand check again when it fires for your subscription id. - Set the perk from the latest result each time,
trueorfalse. Keep it in a server table or a server-set attribute, not as a permanent flag in saved data. - Honour the perk for the whole period the player paid for.
IsSubscribedis true while the subscription is active, andIsRenewingseparately says whether it will renew, so a player who cancels keeps the perk until their paid period ends.
Vulnerable example
Vulnerable
local MarketplaceService = game:GetService("MarketplaceService") local Players = game:GetService("Players") local vipSubscription = "EXP-1111111111" Players.PlayerAdded:Connect(function(player) local status = MarketplaceService:GetUserSubscriptionStatusAsync(player, vipSubscription) player:SetAttribute("Vip", status.IsSubscribed == true) end)
Fixed example
Fixed
local MarketplaceService = game:GetService("MarketplaceService") local Players = game:GetService("Players") local vipSubscription = "EXP-1111111111" local function refresh(player: Player) for attempt = 1, 3 do local ok, status = pcall(MarketplaceService.GetUserSubscriptionStatusAsync, MarketplaceService, player, vipSubscription) if ok then if player.Parent == Players then player:SetAttribute("Vip", status.IsSubscribed == true) end return end task.wait(attempt * 2) end end Players.PlayerAdded:Connect(refresh) Players.UserSubscriptionStatusChanged:Connect(function(player, subscriptionId) if subscriptionId == vipSubscription then refresh(player) end end)
Common false positives
- The client asking the server for its own status through a remote, so it can show a "Subscribed" badge, is fine as long as the server makes the call and nothing on the server trusts the client's answer.
- Rewards granted once per paid period and saved, with the period recorded so they can't be claimed twice, are meant to persist.
- Perks that only change what the player sees on their own screen, like a coloured name tag in their own UI, matter less. Mention them rather than reporting them as a problem.