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.
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.
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)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
| Method | Description |
|---|---|
: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
| Member | Description |
|---|---|
: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) | |
OnUnhandledReceipt | Set 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.
| Server | Client |
|---|---|
: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.
| Event | Fires 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, BundlePurchased | A purchase completed. |
SubscriptionChanged: Signal<(player, subscriptionId, status)> | |
PlusChanged: Signal<(player, isPlus)> |
Prices
| Method | Description |
|---|---|
: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
| Method | Description |
|---|---|
: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
| Method | Description |
|---|---|
:GetRecord(player): Record | Everything 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): PromiseLets 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.
| Member | Description |
|---|---|
: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
| Server | Description |
|---|---|
: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.
| Member | Description |
|---|---|
: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, PromptRefused | Signals. |
Client requests are rate-limited (ClientRequestRatePerSec). Granting always happens on the server; the client can only open prompts.
Configuration
DefaultConfiguration/Services/MonetizationService24 settings| Setting | Default | What 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. |
| PurchaseHistorySize | 50 | Most recent developer-product receipts kept in the record. |
| HandleReceipts | true | Own developer-product receipts; off when another script is the receipt owner. |
| LogToStatistics | true | Forward granted products, bought bundles and Plus sign-ups to StatisticsService. |
| ProductInfoCacheSeconds | 300 | Seconds a product info answer is reused. |
| OwnershipCacheSeconds | 120 | Seconds an ownership answer is reused per player. |
| SubscriptionCacheSeconds | 60 | Seconds a subscription status is reused per player. |
| PromptTimeoutSeconds | 180 | Seconds a Prompt*Async waits for the prompt to close before rejecting. |
| PrefetchOnJoin | true | Look up price level, country and Plus details when a player joins. |
| RegionalPricing | true | Use the country table when Roblox gives no price level for a player. |
| PlusDiscountPercent | 10 | Roblox Plus discount in a subscriber's first months. |
| PlusDiscountPercentAfter | 20 | Roblox Plus discount from PlusDiscountAfterMonths on. |
| PlusDiscountAfterMonths | 2 | Full months of Plus after which the higher discount applies. |
| TransferMinRobux | 10 | Smallest Robux transfer :PromptRobuxTransferAsync accepts. |
| TransferMaxRobux | 500 | Largest Robux transfer :PromptRobuxTransferAsync accepts. |
| CheckTransferEligibility | true | Ask Roblox whether both users may transfer Robux before :PromptRobuxTransferAsync opens a prompt. |
| TransferEligibilityCacheSeconds | 60 | Seconds a transfer eligibility answer is reused per sender and receiver. |
| RewardedAdCooldownSeconds | 30 | Seconds between rewarded video ads a client may request. |
| ReplicateCatalog | true | Send each player the definitions they may be sold. |
| ClientRequestRatePerSec | 2 | Per-player rate limit on client requests. |
| ClientRequestBurst | 5 | Burst that request limiter allows. |
| ClientTimeout | 10 | Seconds the client waits for the catalog or its session before rejecting. |
| HideAdUnitsWhenNotAllowed | false | Destroy 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