Allowing multiple ProcessReceipts is restricted by Roblox for many reasons. However, if you dare to bark on your adventure for multiple well here I am to guide you.
Create an ordinary script within ServerScriptService and paste the following code within there:
shared.MarketplaceService = setmetatable({}, {
__index = function(self, key)
return self[key]
end,
__newindex = function(self, key, value)
if key ~= "ProcessReceipt" then return nil end
rawset(self, tostring(value), value)
rawset(self, "ProcessReceipt", nil)
end,
})
game:GetService("MarketplaceService").ProcessReceipt = function(receiptInfo)
local returnedValue;
for _, callback in shared.MarketplaceService do
if returnedValue then
if (typeof(returnedValue) == "EnumItem" or returnedValue.EnumType == Enum.ProductPurchaseDecision) then
return returnedValue
end
end
task.spawn(function()
returnedValue = callback(receiptInfo)
end)
end
local timeout = tick() + 10
repeat task.wait() until tick() >= timeout or returnedValue
local isCorrectType = (typeof(returnedValue) == "EnumItem" and returnedValue.EnumType == Enum.ProductPurchaseDecision)
return isCorrectType and returnedValue or Enum.ProductPurchaseDecision.NotProcessedYet
end
Now within any server-sided script write the following:
--[[OLD]]
game:GetService("MarketplaceService").ProcessReceipt = function(receiptInfo)
-- Code
end
--[[NEW]]
shared.MarketplaceService.ProcessReceipt = function(receiptInfo)
-- Code
end
How ProcessReceipt Is Handled Internally
We use metamethods and a global table to capture all assignments made to the ProcessReceipt property, and store each one as a separate function entry with keys by its stringified representation. This allows multiple ProcessReceipt functions to coexist and be executed safely.
Example result after two assignments:
shared.MarketplaceService = {
["function: 0x109da0146f5b1e74"] = function(receiptInfo)
-- Your logic here
end,
["function: 0x15902a582f928273"] = function(receiptInfo)
-- Your logic here
end,
}
Behind the Scenes: Metatable Logic
We override the default behavior of the shared.MarketplaceService global by attaching a metatable. This metatable captures any assignment to the ProcessReceipt key and instead saves the given function under a stringified version of itself, effectively treating it as a unique key. The ProcessReceipt field remains nil.
shared.MarketplaceService = setmetatable({}, {
__index = function(self, key)
return self[key]
end,
__newindex = function(self, key, value)
if key ~= "ProcessReceipt" then return end
rawset(self, tostring(value), value)
rawset(self, "ProcessReceipt", nil)
end,
})
For those unaware, rawset() is used to set a value within a metatable without triggering __newindex. We use this so we don’t cause an infinite loop and what we call a C stack overflow.
We disregard attempts to modify keys which isn’t the ProcessReceipt key as we intended.
When the server starts, shared.MarketplaceService is just an empty table:
shared.MarketplaceService = {} -- On launch
Backend Execution Logic
Now that we have a list of ProcessReceipt handlers stored inside shared.MarketplaceService, we can iterate through them each time Roblox triggers the actual purchase callback.
Here’s how we execute those handlers until one returns a valid decision:
game:GetService("MarketplaceService").ProcessReceipt = function(receiptInfo)
local returnedValue
-- Iterate over all stored functions
for _, callback in shared.MarketplaceService do
if returnedValue then
-- If already set, check if it’s a valid ProductPurchaseDecision
if typeof(returnedValue) == "EnumItem" and returnedValue.EnumType == Enum.ProductPurchaseDecision then
return returnedValue
end
end
-- Call each handler in its own thread
task.spawn(function()
returnedValue = callback(receiptInfo)
end)
end
-- Give a wait period to receive decisions
local timeout = tick() + 10
repeat task.wait() until tick() >= timeout or returnedValue
-- Return default if nothing valid was returned
local isValid = (typeof(returnedValue) == "EnumItem" and returnedValue.EnumType == Enum.ProductPurchaseDecision)
return isValid and returnedValue or Enum.ProductPurchaseDecision.NotProcessedYet
end
Converting Existing ProcessReceipt Code
Old method using Roblox’s API directly:
game:GetService("MarketplaceService").ProcessReceipt = function(receiptInfo)
-- Code
end
New method using the extended system:
shared.MarketplaceService.ProcessReceipt = function(receiptInfo)
-- Code
end
No extra setup is required thanks to the metamethods, this function will automatically be added to the internal list and used by the unified handler.
Disclaimer
Sometimes you’ll need to add a wait before your line incase it runs before the actual global variable is set otherwise it’s attempting to index nil with MarketplaceService.
task.wait(); shared.MarketplaceService.ProcessReceipt = function(receiptInfo)
-- Code
end