In short
Create the product in Creator Hub (your experience, Monetization, Developer Products) and copy its ID. A LocalScript calls MarketplaceService:PromptProductPurchase(player, productId) when the button is pressed. One Script in ServerScriptService sets MarketplaceService.ProcessReceipt: it finds the player, gives the item, and returns Enum.ProductPurchaseDecision.PurchaseGranted only when the item really was given, otherwise NotProcessedYet so Roblox tries again later. Record each receipt's PurchaseId so a receipt that arrives twice is granted once.
- Only one script may set
ProcessReceipt. A second one silently replaces the first. - Never grant from
PromptProductPurchaseFinishedon the client; it only says the window closed. - Returning
NotProcessedYetis safe: the purchase is kept and delivered again, for example when the player next joins.
Get the script
Developer product ProcessReceipt script
The one server Script that grants developer products: a handler per product, PurchaseGranted only after a real grant, and each PurchaseId recorded so nothing is granted twice.
-- Script in ServerScriptService named Receipts.
-- The ONLY script that sets MarketplaceService.ProcessReceipt.
local DataStoreService = game:GetService("DataStoreService")
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
local purchaseHistory = DataStoreService:GetDataStore("PurchaseHistory_v1")
local function addCoins(player: Player, amount: number): boolean
local leaderstats = player:FindFirstChild("leaderstats")
local coins = leaderstats and leaderstats:FindFirstChild("Coins")
if coins and coins:IsA("IntValue") then
coins.Value += amount
return true
end
return false -- leaderstats not ready yet: the purchase stays pending and comes back later
end
-- product ID -> grant function. Return true ONLY once the item is really given.
-- These run inside UpdateAsync, so they must not wait or yield.
local PRODUCTS: { [number]: (player: Player) -> boolean } = {
[0000000] = function(player: Player): boolean -- 100 Coins
return addCoins(player, 100)
end,
[1111111] = function(player: Player): boolean -- Full heal
local humanoid = player.Character and player.Character:FindFirstChildOfClass("Humanoid")
if humanoid and humanoid.Health > 0 then
humanoid.Health = humanoid.MaxHealth
return true
end
return false
end,
}
local function processReceipt(receiptInfo: { [string]: any }): Enum.ProductPurchaseDecision
local purchaseId = tostring(receiptInfo.PurchaseId)
local ok, recorded = pcall(function()
return purchaseHistory:UpdateAsync(purchaseId, function(alreadyGranted: boolean?): boolean?
if alreadyGranted then
return true -- delivered again after an earlier grant: do not grant twice
end
local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
local grant = PRODUCTS[receiptInfo.ProductId]
if not (player and grant and grant(player) == true) then
return nil -- cancel the write: the purchase stays pending and is retried
end
return true
end)
end)
if ok and recorded == true then
return Enum.ProductPurchaseDecision.PurchaseGranted
end
if not ok then
warn("[Receipts] purchase " .. purchaseId .. " not processed:", recorded)
end
return Enum.ProductPurchaseDecision.NotProcessedYet
end
MarketplaceService.ProcessReceipt = processReceipt
RoCode builds it in the place you have open in Roblox Studio, on Windows or macOS with a desktop browser; on a phone the button sends the prompt and the link to your computer. Free to start, with free credits every day. More Roblox Studio scripts
Where to put it
ServerScriptService
Receipts Script new
StarterGui
ShopGui ScreenGui
BuyCoins TextButton
BuyCoins LocalScript new
Settings you can change
PurchaseHistory_v1In the script- The data store that remembers which purchases were already granted. Keep the name once the game is live.
Replace the product IDs in PRODUCTS with your own from Creator Hub. Turn on Enable Studio Access to API Services to test the data store in Studio.
What a cheater can and cannot doItems are granted only by the server after Roblox confirms the purchase, and a purchase is recorded before it is reported as granted, so it can be neither faked from the client nor granted twice.
Type-checked with luau-lsp strict against Roblox's API definitions on 5 November 2026. Not playtested in Studio yet: press Play and watch the Output window before you publish.
Prompt for RoCode
Add a developer product shop: a LocalScript on my BuyCoins button prompts product 0000000, and one server Script sets ProcessReceipt, gives 100 coins in leaderstats, records each PurchaseId with UpdateAsync so a purchase is never granted twice, and returns PurchaseGranted only after a real grant.
On this page
How a developer product purchase works
- The player presses your button. A LocalScript calls
PromptProductPurchaseand Roblox shows its purchase window. - The player confirms. Roblox takes the Robux and sends your server a receipt: who bought (
PlayerId), what (ProductId) and a uniquePurchaseId. - Roblox calls your
ProcessReceiptfunction with that receipt. You grant the item and returnPurchaseGranted. - Until Roblox gets
PurchaseGrantedback, the purchase counts as not delivered. Roblox callsProcessReceiptagain for it later, for example the next time the player joins a server.
| Developer product | Game pass | |
|---|---|---|
| Bought | Any number of times | Once, owned forever |
| Who remembers it | Your game must deliver and save it | Roblox, via UserOwnsGamePassAsync |
| Good for | Coins, revives, boosts, spins | VIP, double coins forever, a special tool |
| Guide | This page | Game pass guide |
Step 1: Create the product
- Publish your place. Open the Creator Dashboard, select your experience, and go to Monetization, then Developer Products.
- Click Create a Developer Product, give it a name ("100 Coins"), an icon and a price in Robux, and save.
- Copy the product's ID from the list. You will paste it into both scripts below.
Step 3: Grant purchases on the server
Insert a Script in ServerScriptService named Receipts. It must be the only script in your game that sets ProcessReceipt. Each product gets a small grant function that returns true only when it really gave the item.
-- Script in ServerScriptService named Receipts.
-- The ONLY script that sets MarketplaceService.ProcessReceipt.
local DataStoreService = game:GetService("DataStoreService")
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
local purchaseHistory = DataStoreService:GetDataStore("PurchaseHistory_v1")
local function addCoins(player: Player, amount: number): boolean
local leaderstats = player:FindFirstChild("leaderstats")
local coins = leaderstats and leaderstats:FindFirstChild("Coins")
if coins and coins:IsA("IntValue") then
coins.Value += amount
return true
end
return false -- leaderstats not ready yet: the purchase stays pending and comes back later
end
-- product ID -> grant function. Return true ONLY once the item is really given.
-- These run inside UpdateAsync, so they must not wait or yield.
local PRODUCTS: { [number]: (player: Player) -> boolean } = {
[0000000] = function(player: Player): boolean -- 100 Coins
return addCoins(player, 100)
end,
[1111111] = function(player: Player): boolean -- Full heal
local humanoid = player.Character and player.Character:FindFirstChildOfClass("Humanoid")
if humanoid and humanoid.Health > 0 then
humanoid.Health = humanoid.MaxHealth
return true
end
return false
end,
}
local function processReceipt(receiptInfo: { [string]: any }): Enum.ProductPurchaseDecision
local purchaseId = tostring(receiptInfo.PurchaseId)
local ok, recorded = pcall(function()
return purchaseHistory:UpdateAsync(purchaseId, function(alreadyGranted: boolean?): boolean?
if alreadyGranted then
return true -- delivered again after an earlier grant: do not grant twice
end
local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
local grant = PRODUCTS[receiptInfo.ProductId]
if not (player and grant and grant(player) == true) then
return nil -- cancel the write: the purchase stays pending and is retried
end
return true
end)
end)
if ok and recorded == true then
return Enum.ProductPurchaseDecision.PurchaseGranted
end
if not ok then
warn("[Receipts] purchase " .. purchaseId .. " not processed:", recorded)
end
return Enum.ProductPurchaseDecision.NotProcessedYet
end
MarketplaceService.ProcessReceipt = processReceipt
| Case | Result |
|---|---|
| New purchase, item given | Records the purchase ID, returns PurchaseGranted. |
| The same purchase ID again | Sees it was already granted, gives nothing, returns PurchaseGranted. |
| Player left before the grant | Writes nothing, returns NotProcessedYet. Roblox delivers it next time they join. |
| Unknown product ID | NotProcessedYet. Add the product to PRODUCTS. |
| Data store down | NotProcessedYet. Nothing is lost; it is retried. |
UpdateAsync returning nil from its function cancels the write, which is how "not granted" leaves no record. The grant runs inside that function, so it must be quick and must not wait: add to a value, set an attribute, give a Tool from ServerStorage. Something slow, such as saving the player's whole profile, belongs in your normal save code.
The newer BindReceiptHandler
Roblox's MarketplaceService reference now describes MarketplaceService:BindReceiptHandler as a newer way to handle developer product receipts, with a handler per list of product IDs, and calls ProcessReceipt the legacy route. A bound handler takes precedence and receipts it does not match fall through to ProcessReceipt, so both can live side by side while you move over. The rule from this guide does not change: record the PurchaseId with the grant, and only report success after a real grant. ProcessReceipt still works, and almost every existing game uses it.
Step 4: Test it
- In Studio, open Game Settings, Security, and turn on Enable Studio Access to API Services, so the data store works.
- Press Play and click the button. Studio shows a test purchase window; buying in Studio does not charge Robux.
- Confirm, and check that Coins went up by 100 once.
- In the live game, buy the cheapest product once with a real account and check it arrives. That is the only test of the full path.
Common errors and fixes
Open the Output window (Window menu, then Output) before you press Play.
The purchase window opens but nothing is given
The receipt handler did not grant: the product ID in PRODUCTS does not match, leaderstats or Coins is missing, or the data store failed (look for a [Receipts] warning). Also search your place for ProcessReceipt: if two scripts set it, only the last one runs.
Can only register ProcessReceipt callback on server
A LocalScript sets ProcessReceipt. Move it to a server Script; see the error page.
Studio API access is not enabled (DataStore 403)
Turn on Enable Studio Access to API Services in Game Settings, Security. See the error page.
Players say they paid twice and got it once
Each purchase has its own PurchaseId, so two real purchases are two grants. If one went missing, it is still pending: Roblox delivers it when the handler next returns PurchaseGranted for it, usually on their next join.
Build it with RoCode
If you would rather not set this up by hand, RoCode can build it in the place you have open. RoCode is an independent AI agent for Roblox Studio, not made by Roblox: you describe the change in a web chat, and its Studio plugin creates the scripts and instances in your Explorer, so there is no code to paste. You still create the products themselves in Creator Hub.
Add developer products to my shop: a LocalScript on each buy button prompts the product, and one server Script handles ProcessReceipt, grants 100, 500 or 1200 coins by product ID, records each PurchaseId with UpdateAsync, and returns PurchaseGranted only after a real grant.
- Can check first with Search Scripts whether something in your place already sets ProcessReceipt, so it extends that handler instead of adding a second one
- Writes the Script and LocalScripts with Create Script and is told to compile-check each one with Check Script
- Sets an undo point before and after each batch of changes, so Ctrl+Z steps back one batch at a time
Paste your real product IDs into the prompt, and buy each product once in Studio and once in the live game. How RoCode connects to Studio.
Questions
What is a developer product in Roblox?
An item players can buy many times for Robux, such as coins or a revive. Your game delivers it in ProcessReceipt each time.
Where does ProcessReceipt go?
In one Script in ServerScriptService. Only one script may set it, and never a LocalScript.
What should ProcessReceipt return?
Enum.ProductPurchaseDecision.PurchaseGranted once the item was given and recorded, and NotProcessedYet in every other case.
Why record the PurchaseId?
Because the same receipt can be delivered again, for example after a server crash. Recording the ID with the grant means a repeat is recognised and not granted twice.
Do purchases in Studio cost Robux?
No. Studio shows a test purchase window and charges nothing. Test once in the live game too.
Sources and further reading
- Roblox Creator Docs: Developer products. creating products, prompting and handling receipts
- Roblox Creator Docs: MarketplaceService.ProcessReceipt. the receipt fields and the retry behaviour
- Roblox Creator Docs: MarketplaceService:BindReceiptHandler
- Roblox Creator Docs: GlobalDataStore:UpdateAsync. returning nil to cancel the write
How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 5 November 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.