ProcessReceipt and the Two-State Problem: Why Developer Products Cannot Handle Invalid Purchases
The ProcessReceipt callback supports only two return values: PurchaseGranted and NotProcessedYet. There is no mechanism to reject a purchase and trigger a refund. This two-state contract creates a class of unresolvable failure modes where players lose Robux with no recourse.
The Current API Contract
When a developer product is purchased, Roblox invokes the ProcessReceipt callback with the receipt. The developer's code must return one of two values:
PurchaseGranted— The purchase was successfully applied. Roblox marks the receipt as complete.NotProcessedYet— The purchase was not applied. Roblox will retry the callback the next time the player joins a server or makes another purchase in the same server.
There is no third option. If the purchase is permanently invalid — not temporarily delayed, but fundamentally unable to be granted under any circumstances — the developer has no way to communicate this to the platform. The only choices are to grant something the player shouldn't receive, or to defer indefinitely via NotProcessedYet and hope the receipt eventually stops retrying. The player is never refunded.
Previously, Roblox documentation indicated that unprocessed receipts would result in an automatic refund after 72 hours. This is no longer the documented behavior, and the change appears to have been made without a formal announcement. Unprocessed receipts now retry under limited conditions with no guaranteed refund.
Failure Modes the Current Contract Cannot Express
The following scenarios all share a common structure: the purchase was valid when the player saw the prompt, but by the time ProcessReceipt executes, the conditions for granting it no longer hold.
1. Target player leaves during a gifting or interaction purchase
Player A opens a prompt to gift an item to Player B. While the prompt is open, Player B leaves the server. Player A completes the purchase. ProcessReceipt fires, but Player B no longer exists in the session. The gift cannot be delivered. Returning PurchaseGranted would be incorrect. Returning NotProcessedYet causes the receipt to retry on rejoin, at which point the context is lost.
2. Double transaction on a single-use developer product
Due to service latency or prompt behavior, the player triggers two transactions before the first is processed. The first receipt is granted correctly. The second receipt arrives for an item the player already owns. The developer cannot grant it again. Returning NotProcessedYet just delays the inevitable — the receipt will never be valid. Gamepasses are not a substitute because they cannot enforce prerequisites, cannot be scoped to in-game-only flows, and are purchasable directly from the website.
3. Time-limited currency or event item purchased before a DataStore failure
A player purchases event-specific currency. The DataStore call to grant it fails, so ProcessReceipt returns NotProcessedYet. By the time the player rejoins and the receipt retries, the event has ended. The currency is no longer usable. Granting it would give the player a worthless item. Rejecting it and refunding is the correct action, but the API does not allow it.
4. Limited-stock developer product sold out between prompt and processing
A developer product sells a limited number of copies. When the prompt was shown, the item was in stock. By the time the receipt is processed, all copies have been claimed. The purchase cannot be fulfilled. The player should be refunded, but the only response is NotProcessedYet, which retries a purchase that will never become valid.
5. Ineligible purchase surfaced by mistake
A developer product is shown to a player who does not meet eligibility criteria (e.g., a starter pack shown to a veteran player due to a UI bug). The player purchases it. The server detects the ineligibility in ProcessReceipt. Granting it would violate the game's rules. The developer has no way to reject it and return the player's Robux.
The NotProcessedYet Trap
In all of the above scenarios, the instinct is to return NotProcessedYet as a "soft reject." In practice, this does not resolve the situation. It causes Roblox to retry the receipt under two conditions: the player joins a server again, or the player makes another purchase in the same server session. Neither guarantees timely resolution, and in most scenarios above, retrying will never produce a different outcome — the receipt is permanently invalid, not temporarily delayed.
The result is that the player's Robux are consumed, the purchase is never granted, no refund is issued, and the receipt sits in permanent limbo. From the player's perspective, they paid for something and received nothing. From the developer's perspective, there is no mechanism to make it right.
What the API Needs
The core issue is that the two-state return value cannot represent a permanently invalid purchase. The following changes would address the failure modes described above:
1. PurchaseRejected enum value
A third return value for ProcessReceipt that tells the platform the purchase is permanently invalid and the player should be refunded immediately. This is the most direct solution and addresses every failure mode above.
2. Single-purchase developer products
An option on the developer product configuration that limits it to one purchase per player, enforced platform-side. This eliminates the double-transaction failure mode without requiring developers to build their own deduplication logic in DataStore.
3. Escrow-based processing window
Keep the Robux in escrow until the receipt reaches a terminal state (PurchaseGranted or PurchaseRejected), with an expiration after which unresolved receipts are automatically refunded. This restores the safety net that the removed 72-hour refund previously provided.
Of these, option 1 (PurchaseRejected) is the highest priority. It is the only change that gives developers the ability to resolve permanently invalid purchases at the moment they are detected, rather than deferring to a system that was not designed to handle them.
Current Workarounds and Why They Are Insufficient
- Grant anyway and over-deliver. The developer grants the purchase even when conditions aren't met. This preserves the player's Robux value but violates game logic and can create exploitable inconsistencies.
- Return NotProcessedYet and hope for the best. The receipt retries on rejoin, but if the purchase is permanently invalid, every retry produces the same result. The player's Robux are gone.
- Use gamepasses instead. Gamepasses are one-time purchases, but they cannot enforce prerequisites, cannot be restricted to in-game purchase flows, and are purchasable directly from the website without server-side validation.
- Build a custom premium currency layer. The developer sells a generic currency via developer products and handles all purchase logic internally. This works but is a significant engineering burden that exists solely because the platform's purchase API cannot express a rejection.
Recommendations
- Add
Enum.ProductPurchaseDecision.PurchaseRejectedto the API. This is backward-compatible and immediately resolves every failure mode described in this memo. - Document the removal of the 72-hour automatic refund and clarify what happens to permanently unprocessed receipts under the current system.
- Consider adding a single-purchase option to developer product configuration as a platform-enforced guardrail against double transactions.
- Consider an escrow model where Robux remain held until the receipt reaches a terminal state, with automatic refund on expiration.
← Back to writing