Fix the "ProcessReceipt needs a fix" banner and sell your products outside your game

Roblox is rolling out a feature that shows your developer products and passes outside your game: in your Game Details Page store, on the home page, in search, and on the page players see after they buy Robux. The player buys first and joins your game later with the item in hand. (Announcement, Sep 22 2026)

For developer products, Roblox lists two conditions (passes don’t need this):

  1. The product is marked Listed in Creator Hub → Monetization → Shop.
  2. ProcessReceipt is implemented correctly.

If Roblox detects a Listed product that needs a fix, a banner appears in Monetization → Shop with a report. Products in that report aren’t shown outside your game. Inside your game, nothing changes. You may not see the banner yet, because the feature is still rolling out by account.

Roblox has not said how it decides that a ProcessReceipt is “incorrect”. Points marked (docs) come from Roblox’s official documentation or announcement. Everything else is practical advice. Use this as a checklist, not a guarantee.


Why purchases made outside the game are different

When someone buys inside your game, their data is already loaded: leaderstats, profile, character.

When someone buys outside and then joins, your receipt callback runs when they join the server (docs). Your data system may not have loaded them yet.

If your handler fails at that point and returns NotProcessedYet, the docs say Roblox does not retry on a timer: on that server it only tries again if the player starts another developer product purchase, or when they join a server again (docs). A Roblox engineer said on Sep 21 that receipts are now polled every 10 seconds (bug report thread). Either way, a handler that gives up too early leaves a player who paid, joined, and got nothing, at least for a while.


Checklist: 8 common mistakes

1. No ProcessReceipt at all.
Receipts are auto-acknowledged, and “you can’t get a receipt back after it has been acknowledged” (docs).

2. Granting in PromptProductPurchaseFinished.
Don’t. The event firing “does not mean that a user has successfully purchased an item” (docs). A purchase made outside your game also never shows an in-game prompt, so only ProcessReceipt / BindReceiptHandler will see it.

3. Returning PurchaseGranted when nothing was given.
The example on the Developer Products docs page runs pcall(handler, receipt, player) and returns PurchaseGranted whenever the pcall succeeds. The same docs page says handlers “must return true”. If your handler quietly does nothing (for example, if gold then ... end while leaderstats aren’t loaded), the pcall still succeeds. The purchase is closed and the player gets nothing. Check that the handler actually returned true.

4. Not waiting for player data.
This is the outside-purchase problem described above. The fix is to wait briefly inside the callback until the player’s data is ready, then grant. Keep it under 10 seconds: the docs say a ProcessReceipt callback “can yield for as long as the server is running” (docs), but the same Roblox engineer explained that a handler still running when the next 10 s poll arrives causes “already being processed” errors (thread). If the data isn’t ready in time, return NotProcessedYet and let the next delivery try again.

5. Granting the same purchase twice.
The callback for one purchase “might run on two different servers at the same time”. It can also fail to be recorded even after you return PurchaseGranted, in which case it is delivered again (docs). Claim each PurchaseId atomically (for example with UpdateAsync, or in your session-locked profile) before you grant, and skip purchases you already granted.

6. A Listed product with no handler.
If no code in your game handles a Listed product, that purchase can’t be delivered correctly. Handle every developer product you list.

7. More than one script setting ProcessReceipt.
Roblox says to set the callback “one time in a single server-side Script” (docs). Kits and models that set their own can clash with yours. The newer MarketplaceService:BindReceiptHandler(Enum.ReceiptType.DeveloperProduct, handler, {productIds}) lets each system handle only its own product IDs. Anything that doesn’t match falls through to your existing ProcessReceipt (docs).

8. Listing items that shouldn’t be sold outside your game.
Don’t sell random items (loot boxes) or items limited by time, quantity, place, or user settings and roles outside your game (docs). Keep those Unlisted.


How to test

The official docs describe a test mode for external purchases. It is the closest official way to try a purchase made outside the game (docs):

  1. Creator Hub → Monetization → Developer Products → ⋮ → External Purchase Settings → Enable test mode.
  2. Open a cheap product and tick Allow external purchases. Test purchases cost real Robux (docs).
  3. Buy it from the Store tab of your game page, then join the game.
  4. Check that you received the item.

Then go back to the Shop report, select the products, and confirm they’re fixed.


Free module: ReceiptRouter

ReceiptRouter is a small open module that handles points 3, 4, 5 and 7 for you:

  • Binding: uses BindReceiptHandler filtered to your product IDs, so it never replaces another script’s ProcessReceipt. If BindReceiptHandler doesn’t exist, it falls back to ProcessReceipt. If binding fails because of a conflict, it raises an error instead of silently overwriting anything.
  • Waiting: waits up to 8 s for your data to load (under the 10 s receipt poll), then returns NotProcessedYet so the next delivery tries again. You define what “ready” means, and you can change WaitSeconds.
  • Confirming: returns Processed only when your grant function returned true.
    • If it returns false or errors, the router retries every second.
    • If it returns nothing (the classic bug), it warns and does not retry, so the item is never handed out over and over.
  • No double grants: claims each PurchaseId in a DataStore with UpdateAsync before granting, so a second server or a redelivery won’t grant it again. A claim abandoned by a crashed server is taken over after 2 minutes.
  • Clear warnings: the output says exactly what’s wrong (missing return true, data never loaded, DataStore errors).
-- Script in ServerScriptService
local ReceiptRouter = require(game.ServerScriptService.ReceiptRouter)

local router = ReceiptRouter.new({
	-- truthy when this player's data is loaded (leaderstats, a ProfileStore profile, etc.)
	IsReady = function(player)
		return player:FindFirstChild("leaderstats")
	end,
})

router:Product(123456789, function(player, receipt)   -- 500 Coins
	player.leaderstats.Coins.Value += 500
	return true   -- only after giving it
end)

router:Product(987654321, function(player, receipt)   -- 50 Gems (saved by your data system)
	player:SetAttribute("Gems", (player:GetAttribute("Gems") or 0) + 50)
	return true
end)

router:Start()

Using ProfileStore or ProfileService? Make IsReady return Profiles[player] and add to the profile’s data inside the grant function.

Rules for grant functions:

  • Return true once the item was given.
  • Return false (or error) if it couldn’t be given. Nothing must have been given in that case.
  • Saving is your game’s job, like any other reward.
  • In bind mode, register every Listed product. A product you don’t register falls through to your ProcessReceipt. If you don’t have one, Roblox auto-acknowledges it.

Get it: ReceiptRouter on the Creator Store. It’s free and open: read every line before you use it.

Tested

The module was tested offline in 65 checks covering:

  • the player not being in the server, data loading late or never, and the player leaving while it waits;
  • a grant without return true, a grant returning false and then true, and errors followed by retries;
  • a redelivered receipt, two servers handling the same purchase at once, and an abandoned claim;
  • temporary and permanent DataStore errors, and Studio without API access;
  • a binding conflict, and the ProcessReceipt fallback;
  • the default wait staying under 10 seconds.

Please still run test mode in your own game before confirming the fix.


Sources


I also make Flash Offers Kit, which uses this same receipt logic since v1.1. Happy to answer any questions below.


Edit, Oct 3: ReceiptRouter v1.0.1. Default wait is now 8 s instead of 60 s, after Roblox staff mentioned the 10 s receipt poll. Points 4 and “Why purchases made outside the game are different” updated.

Update: ReceiptRouter v1.0.1

A Roblox engineer explained in Developer Product Receipts Are Permanently Stuck that receipts are now polled every 10 seconds, and a handler that is still running when the next poll arrives causes “already being processed” errors.

v1.0.0 waited up to 60 s for player data, so a slow data load could trigger those errors. In v1.0.1:

  • The default WaitSeconds is now 8 s. If the data isn’t ready by then, it returns NotProcessedYet and the next delivery tries again.
  • You can still set WaitSeconds yourself.
  • 65 offline checks (3 new).

Flash Offers Kit and Flash Offers Lite now ship the same module. Point 4 of the guide above is updated too.