---
name: roblox-purchase-safety
description: Use when adding or changing developer products, game passes or anything in MarketplaceService. Makes purchase handling reliable so players always get what they paid for exactly once.
---

# Safe Robux purchases

Lost or doubled purchases cost real money and trust. Follow these rules for every product.

## Developer products (ProcessReceipt)

- Set `MarketplaceService.ProcessReceipt` **once**, in one server script. If two scripts set it, only one of them
  wins.
- Inside the callback:
  1. Find the player from `receiptInfo.PlayerId`. If they've left, return
     `Enum.ProductPurchaseDecision.NotProcessedYet`.
  2. If `receiptInfo.PurchaseId` was already granted (it's stored in the player's saved data), return
     `PurchaseGranted` without granting again.
  3. Look up the reward by `receiptInfo.ProductId` in a server-side table.
  4. Grant it, record the `PurchaseId`, and **save**.
  5. Return `PurchaseGranted` only after the save succeeds. If anything fails, return `NotProcessedYet` so Roblox
     retries later.
- Never grant from the client, and never grant from `PromptProductPurchaseFinished`: it isn't proof of payment.

## Game passes

- Check ownership on the server with `MarketplaceService:UserOwnsGamePassAsync(userId, passId)`, inside `pcall`.
  Cache the result per session.
- Grant the perk in `PromptGamePassPurchaseFinished` on the server when `wasPurchased` is true, and on join when
  they already own it.

## Prompts

- The client may only *ask* for a prompt. Prices, IDs and rewards live in server tables.
- Call `PromptProductPurchase` or `PromptGamePassPurchase` from a server script or a validated client request.

## Before you finish

Explain what happens if the player leaves mid-purchase, if the save fails, and if Roblox calls `ProcessReceipt`
twice with the same `PurchaseId`.
