Tutorial · Rewards, pets and trading

How to make a developer product in Roblox

A developer product is something players can buy again and again for Robux: a pack of coins, a revive, a speed boost for ten minutes. Unlike a game pass, Roblox does not remember that they own it, so your game must deliver it, and deliver it exactly once, even if the server crashes halfway. This guide creates a product, adds a buy button, and writes the one server handler that grants it, with the purchase ID check that stops both lost purchases and double grants.

Updated 5 November 20267 min readIntermediateLuauWritten by

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 PromptProductPurchaseFinished on the client; it only says the window closed.
  • Returning NotProcessedYet is 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
-- 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

Explorer: the receipt script and a buy button
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
  1. Get the script
  2. How it works
  3. 1. Create it
  4. 2. Buy button
  5. 3. ProcessReceipt
  6. BindReceiptHandler
  7. 4. Test it
  8. Common errors
  9. With RoCode
  10. Questions
  11. Sources

How a developer product purchase works

  1. The player presses your button. A LocalScript calls PromptProductPurchase and Roblox shows its purchase window.
  2. The player confirms. Roblox takes the Robux and sends your server a receipt: who bought (PlayerId), what (ProductId) and a unique PurchaseId.
  3. Roblox calls your ProcessReceipt function with that receipt. You grant the item and return PurchaseGranted.
  4. Until Roblox gets PurchaseGranted back, the purchase counts as not delivered. Roblox calls ProcessReceipt again for it later, for example the next time the player joins a server.
Developer product or game pass?
Developer productGame pass
BoughtAny number of timesOnce, owned forever
Who remembers itYour game must deliver and save itRoblox, via UserOwnsGamePassAsync
Good forCoins, revives, boosts, spinsVIP, double coins forever, a special tool
GuideThis pageGame pass guide

Step 1: Create the product

  1. Publish your place. Open the Creator Dashboard, select your experience, and go to Monetization, then Developer Products.
  2. Click Create a Developer Product, give it a name ("100 Coins"), an icon and a price in Robux, and save.
  3. Copy the product's ID from the list. You will paste it into both scripts below.

Step 2: The buy button

In StarterGui, add a ScreenGui named ShopGui with a TextButton named BuyCoins, and put this LocalScript inside the button:

LocalScript in the BuyCoins button named BuyCoins
-- LocalScript inside the BuyCoins TextButton.
-- Only shows the purchase window. The server gives the coins (Receipts script).
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")

local COINS_100_ID = 0000000 -- your developer product ID

local button = script.Parent :: TextButton

button.Activated:Connect(function()
	MarketplaceService:PromptProductPurchase(Players.LocalPlayer, COINS_100_ID)
end)

Nothing here gives coins. The GUI button guide covers styling the button and opening a shop panel; the shop GUI guide builds a whole shop.

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
-- 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
What the handler does in each case
CaseResult
New purchase, item givenRecords the purchase ID, returns PurchaseGranted.
The same purchase ID againSees it was already granted, gives nothing, returns PurchaseGranted.
Player left before the grantWrites nothing, returns NotProcessedYet. Roblox delivers it next time they join.
Unknown product IDNotProcessedYet. Add the product to PRODUCTS.
Data store downNotProcessedYet. 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

  1. In Studio, open Game Settings, Security, and turn on Enable Studio Access to API Services, so the data store works.
  2. Press Play and click the button. Studio shows a test purchase window; buying in Studio does not charge Robux.
  3. Confirm, and check that Coins went up by 100 once.
  4. 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.

You type

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.

RoCode does
  • 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

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.

Try RoCode on your own place.

Free every day, no card required. Install the plugin, describe the feature, and review what lands in Studio.