Skip to content
Engine

Data and Security

MonetizationService

Developer products, game passes, subscriptions, avatar items, bundles and commerce products with safe receipts, regional prices, Roblox Plus, policy checks, rewarded ads and a purchase ledger.

ServiceServerClient

Getting it

local MonetizationService = Engine:GetService("MonetizationService")

Overview

MonetizationService handles everything between "show a buy button" and "the player has their item":

  • A catalog of what you sell, with names, prices and your own data, replicated to each player.
  • Prompts that return a Promise resolving with whether the player bought.
  • Receipts that grant developer products exactly once, stored in the player's DataService profile so a purchase can never be granted twice, even across servers.
  • Prices with Roblox price levels, regional pricing, discounts and Roblox Plus discounts.
  • Policy checks: products marked as random rewards, ads and subscriptions are only offered to players whose Roblox policy allows them (see PlayerService).
  • Ownership of passes, assets and bundles with caching, subscription status, Robux transfers, rewarded video ads and ad units.
  • A per-player ledger: Robux spent, purchase history, passes, subscriptions and more.
Server: selling coins
local MonetizationService = Engine:GetService("MonetizationService")
local DataService = Engine:GetService("DataService")

local PlayerData = DataService:CreateProfileStore("PlayerData", { Coins = 0 }) -- your player profiles
MonetizationService:BindProfileStore(PlayerData) -- receipts are recorded in these profiles

MonetizationService:DefineProduct(1234567, { Name = "500 Coins", Data = { Coins = 500 } })
MonetizationService:DefinePass(7654321, { Name = "VIP" })

MonetizationService:BindProductHandler(1234567, function(player, receipt, profile)
	profile:Increment("Coins", 500)
	return true -- granted
end)

MonetizationService.PassPurchased:Connect(function(player, passId)
	if passId == 7654321 then
		giveVip(player)
	end
end)
Client: the buy button
local MonetizationService = Engine:GetService("MonetizationService")

MonetizationService:GetPriceAsync("Product", 1234567):andThen(function(price)
	buyButton.Text = string.format("%d R$", price.Price)
end)

buyButton.Activated:Connect(function()
	MonetizationService:PromptProductAsync(1234567)
end)

Where purchases are stored

Receipts and the rest of the ledger are recorded inside each player's profile, under ProfilePath (default "Monetization"), in the profile store you choose: call :BindProfileStore(store) before players join, or name the store in ProfileStoreName. The service never picks a store by itself. Your game starts the player sessions in that store as usual, and the service adds its record to a profile that has none yet, so it does not need to be in your template.

Without a profile store it still works, but keeps the records in memory for the session and remembers granted receipts on that server only, so a purchase could be granted twice on different servers. It warns you the first time it grants a receipt that way. Product handlers then get nil as their profile.

Catalog

MethodDescription
:DefineProduct(productId, options?)A developer product.
:DefinePass(passId, options?)A game pass.
:DefineSubscription(subscriptionId, options?)An experience subscription.
:DefineAsset(assetId, options?), :DefineBundle(bundleId, options?)Avatar items sold in-experience.
:DefineCommerceProduct(id, options?)A commerce product (physical goods).
:GetDefinition(kind, id), :FindDefinition(kind, id), :GetDefinitions(kind?)Look up definitions.
:CanSell(player, kind, id)Whether this player may be offered it (policy check).
:GetSellableDefinitions(player, kind?)Everything this player may be offered.

Options: { Name?, Price?, AdReward?, Random?, Data? }. Random = true marks a paid random reward (loot box); such items are only sold where PaidRandomItems is allowed. AdReward = true marks a product granted by watching a rewarded video ad. Data is free for your own use and replicates with the catalog.

Kinds: "Product", "Pass", "Subscription", "Asset", "Bundle", "CommerceProduct".

Receipts

MemberDescription
:BindProductHandler(productId, handler)handler(player, receipt, profile) grants the product. Return false (or error) to leave the purchase pending; Roblox will retry it later.
:UnbindProductHandler(productId), :HasProductHandler(productId)
OnUnhandledReceiptSet to a function (player, receipt, profile) -> boolean to handle products without a handler.
:CreateReceiptProcessor()The processor itself, for when HandleReceipts is off and another script owns MarketplaceService.ProcessReceipt.
ProductGranted: Signal<(player, productId, receipt)>A product was granted.

With HandleReceipts = true (default) the service sets MarketplaceService.ProcessReceipt itself. Only one script in a game may own that callback.

Prompts

All prompt methods check the player's policy first, open the Roblox purchase prompt, and resolve with (purchased: boolean, details) when it closes. They reject when the policy forbids the purchase (and fire PromptRefused) or after PromptTimeoutSeconds.

ServerClient
:PromptProductAsync(player, productId):PromptProductAsync(productId)
:PromptPassAsync(player, passId):PromptPassAsync(passId)
:PromptAssetAsync(player, assetId):PromptAssetAsync(assetId)
:PromptBundleAsync(player, bundleId):PromptBundleAsync(bundleId)
:PromptSubscriptionAsync(player, id):PromptSubscriptionAsync(id)
:PromptCancelSubscription(player, id):PromptCancelSubscription(id)
:PromptPlusAsync(player):PromptPlusAsync()
:PromptBulkPurchaseAsync(player, items):PromptBulkPurchaseAsync(items)
:PromptCommerceProductAsync(player, id):PromptCommerceProductAsync(id)

Bulk items are { Type = "Asset" | "Bundle", Id, Duration? }. :IsPromptOpen(player, kind, id) tells you whether a prompt is still showing.

EventFires when
PromptFinished: Signal<(player, kind, id, purchased, details)>Any prompt closes.
PromptRefused: Signal<(player, kind, id, policy)>A prompt was blocked by policy.
PassPurchased, AssetPurchased, BundlePurchasedA purchase completed.
SubscriptionChanged: Signal<(player, subscriptionId, status)>
PlusChanged: Signal<(player, isPlus)>

Prices

MethodDescription
:GetPriceAsync(player, kind, id): Promise<PriceInfo>The price this player sees.
:GetProductInfoAsync(kind, id)Roblox product info, cached.
:GetDeveloperProductsAsync()Every developer product of the experience.
:InvalidatePrices(kind?, id?)Forget cached prices.
:GetPriceLevel(player), :GetPriceLevelAsync(player)The player's regional price level.
:GetCountry(player), :GetCountryAsync(player)
:GetRegionalPrice(price, levelOrCountry?)What a list price becomes at a price level or in a country.
:GetPlusDiscountPercent(player)Their Roblox Plus discount, if any.

PriceInfo: { Price, ListPrice, RegionalPrice, PriceLevel, Country?, IsForSale, IsRegional, IsDiscounted, Discount, DiscountPercent, Discounts, Source }. Source is "Live" when it comes from Roblox or "Estimate" when Engine calculated it from the regional table.

Ownership and subscriptions

MethodDescription
:OwnsPassAsync(player, passId), :OwnsAssetAsync(player, assetId), :OwnsBundleAsync(player, bundleId)Cached for OwnershipCacheSeconds.
:OwnsAsync(kind, player, id), :Owns(kind, player, id)Generic, and the cached answer (or nil).
:InvalidateOwnership(player?, kind?, id?)
:GetSubscriptionStatusAsync(player, id){ IsSubscribed, IsRenewing, State?, NextRenewTime?, ExpireTime?, ExpirationReason? }.
:GetSubscriptionHistoryAsync(player, id), :IsSubscribed(player, id)
:IsPlus(player), :IsPremium(player), :IsSubscriber(player)
:GetPlusDetailsAsync(player){ IsSubscribed, StartTime?, IsOriginExperience, Months, DiscountPercent }.

The ledger

MethodDescription
:GetRecord(player): RecordEverything recorded for the player.
:GetRobuxSpent(player)Total Robux spent on developer products.
:GetPurchases(player)The last PurchaseHistorySize purchases: { Id, Product, Robux, Time, Channel?, Place? }.
:BindProfileStore(store)Keep the records in this profile store. Call it before the first player joins; see Where purchases are stored.
:HasProfile(player), :GetProfileStore()Whether the player's record is kept in a profile, and the bound store (nil when none is bound).
ProfileStoreBound: Signal<store>

A Record holds Purchases, RobuxSpent, PurchaseCount, FirstPurchase, LastPurchase, Passes, Subscriptions, transfer totals, AdRewards and Plus details. With LogToStatistics, purchases are also reported to StatisticsService.

Robux transfers

MonetizationService:PromptRobuxTransferAsync(sender: Player, receiverUserId: number, amount: number): Promise

Lets a player send Robux to another user (between TransferMinRobux and TransferMaxRobux). TransferSent and TransferReceived fire with { TransferRequestId, Direction, Amount?, SenderUserId?, ReceiverUserId?, Receipt }.

Before it opens the prompt, the service asks Roblox whether both users may transfer Robux. When one of them may not, it rejects with "SenderNotEligible" or "ReceiverNotEligible" without opening a prompt, and fires TransferRefused. When Roblox cannot tell, the prompt opens as usual. Turn the check off with CheckTransferEligibility = false.

MemberDescription
:GetRobuxTransferEligibilityAsync(sender, receiverUserId)Resolves with "Eligible", "SenderNotEligible", "ReceiverNotEligible" or "Unknown".
:CanTransferRobuxAsync(sender, receiverUserId)Resolves true only when Roblox answers "Eligible". Use it to show or hide a gift button.
TransferRefused: Signal<(sender, receiverUserId, reason)>The eligibility check stopped a prompt. reason is "SenderNotEligible" or "ReceiverNotEligible".

Both methods use MarketplaceService:CanUsersTransferRobuxAsync, and the answer is cached per sender and receiver for TransferEligibilityCacheSeconds. A player is never eligible to send Robux to themselves.

Ads

ServerDescription
:ShowRewardedVideoAsync(player, productId, placementId?)Shows a rewarded video; on completion the product is granted through its handler. Limited to one per RewardedAdCooldownSeconds.
:IsAdReward(productId)
:CreateAdGui(part, { Face?, Video?, FallbackImage? }?)Places an immersive ad unit on a part.
:GetAdUnits(root?)
:GetCampaignEligibilityAsync(player, campaignId)
AdShown: Signal<(player, productId, result)>, AdRewardGranted: Signal<(player, productId, receipt)>

On the client: :IsRewardedAdAvailableAsync(), :WatchRewardedAdAsync(productId, placementId?), :IsAdIneligibleResult(name), :RegisterAdOpportunity(button, placementId?), :RegisterDisclosureButton(button, campaignId), :GetAdUnits(root?) and :HideAdUnits(root?). Ads are only shown where the player's policy allows them; with HideAdUnitsWhenNotAllowed ad units are removed for those players.

Sessions

When a player joins, the service looks up their price level, country and Plus details (PrefetchOnJoin) and fires SessionReady: Signal<(player, SessionInfo)>. :GetSessionInfo(player) returns { PriceLevel, Country?, Plus?, StoreName?, ProfilePath }.

IsPrivateServer, IsReservedServer and PrivateServerOwnerId describe the current server, handy for private-server perks.

:PromptCreateAvatarAsync(player, tokenId, humanoidDescription) wraps Roblox's avatar creation prompt.

Client

The client gets a read-only view of the catalog and its own session, plus prompts and prices for the local player.

MemberDescription
:GetCatalogAsync(timeout?), :GetCatalog()The definitions this player may buy (ReplicateCatalog).
:GetDefinition(kind, id), :FindDefinition(kind, id), :GetDefinitions(kind?)
:CanBuy(kind, id)Whether the item is offered to this player.
:GetSession(), :WaitForSessionAsync(timeout?), :GetPriceLevel(), :GetCountry()
:GetPriceAsync(kind, id), :GetRegionalPrice(price, level?), :GetProductInfoAsync(kind, id), :InvalidatePrices()
:OwnsPassAsync, :OwnsAssetAsync, :OwnsBundleAsync, :OwnsAsync(kind, id)
:GetSubscriptionInfoAsync(id), :GetStateAsync(), :IsPlus(), :IsPremium(), :GetPlusDetailsAsync()
:RankProductsAsync(products), :RecommendTopProductsAsync(kinds?)Roblox's personalised product ranking.
CatalogChanged, SessionChanged, ProductGranted(productId, channel?), PromptFinished, PassPurchased, AssetPurchased, BundlePurchased, PlusChanged, PromptRefusedSignals.

Client requests are rate-limited (ClientRequestRatePerSec). Granting always happens on the server; the client can only open prompts.

Configuration

Defaults from DefaultConfiguration/Services/MonetizationService24 settings
SettingDefaultWhat it does
ProfileStoreName""ProfileStore whose player profiles hold the Monetization record; empty keeps the records in memory.
ProfilePath"Monetization"Path inside a profile's Data the record lives under.
PurchaseHistorySize50Most recent developer-product receipts kept in the record.
HandleReceiptstrueOwn developer-product receipts; off when another script is the receipt owner.
LogToStatisticstrueForward granted products, bought bundles and Plus sign-ups to StatisticsService.
ProductInfoCacheSeconds300Seconds a product info answer is reused.
OwnershipCacheSeconds120Seconds an ownership answer is reused per player.
SubscriptionCacheSeconds60Seconds a subscription status is reused per player.
PromptTimeoutSeconds180Seconds a Prompt*Async waits for the prompt to close before rejecting.
PrefetchOnJointrueLook up price level, country and Plus details when a player joins.
RegionalPricingtrueUse the country table when Roblox gives no price level for a player.
PlusDiscountPercent10Roblox Plus discount in a subscriber's first months.
PlusDiscountPercentAfter20Roblox Plus discount from PlusDiscountAfterMonths on.
PlusDiscountAfterMonths2Full months of Plus after which the higher discount applies.
TransferMinRobux10Smallest Robux transfer :PromptRobuxTransferAsync accepts.
TransferMaxRobux500Largest Robux transfer :PromptRobuxTransferAsync accepts.
CheckTransferEligibilitytrueAsk Roblox whether both users may transfer Robux before :PromptRobuxTransferAsync opens a prompt.
TransferEligibilityCacheSeconds60Seconds a transfer eligibility answer is reused per sender and receiver.
RewardedAdCooldownSeconds30Seconds between rewarded video ads a client may request.
ReplicateCatalogtrueSend each player the definitions they may be sold.
ClientRequestRatePerSec2Per-player rate limit on client requests.
ClientRequestBurst5Burst that request limiter allows.
ClientTimeout10Seconds the client waits for the catalog or its session before rejecting.
HideAdUnitsWhenNotAllowedfalseDestroy ad units on a client whose policy forbids ads.

Types

export type Kind = "Product" | "Pass" | "Subscription" | "Asset" | "Bundle" | "CommerceProduct"
export type Id = number | string
export type Definition
export type DefinitionOptions
export type PriceInfo
export type Discount
export type ProductInfo
export type SubscriptionStatus
export type PlusDetails
export type Record
export type PurchaseEntry
export type ReceiptInfo
export type ProductHandler = (player: Player, receipt: ReceiptInfo, profile: any?) -> boolean?
export type PromptKind
export type BulkItem
export type Transfer
export type TransferEligibility = "Eligible" | "SenderNotEligible" | "ReceiverNotEligible" | "Unknown"
export type SessionInfo