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.

warningLuaudit check: subscription-perks

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 GetUserSubscriptionStatusAsync call 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.UserSubscriptionStatusChanged that 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 IsSubscribed is false. 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 GetUserSubscriptionStatusAsync when the player joins, inside pcall, with a retry.
  • Connect Players.UserSubscriptionStatusChanged and check again when it fires for your subscription id.
  • Set the perk from the latest result each time, true or false. 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. IsSubscribed is true while the subscription is active, and IsRenewing separately 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.