Developer Product Receipts
Roblox gives an experience one MarketplaceService.ProcessReceipt callback for all Developer Products. Your game owns that callback, its durable receipt ledger, and the product grant. Visual Novel Creator does not replace it.
A VNC Prompt Purchase node separates two events:
- The Roblox purchase prompt closes.
- Roblox sends your server a receipt and your game grants the product.
Roblox’s PromptProductPurchaseFinished event reports that the prompt closed. Its isPurchased = true value is not purchase confirmation. The story stays pending until your receipt owner reports the matching granted receipt to VNC.
Connect your receipt owner
Section titled “Connect your receipt owner”The installed runtime exposes this server-only ModuleScript:
ServerScriptService.VNCServer.Integrations.DeveloperProductReceiptsCall ReportGranted(receiptInfo) only after your own idempotent grant has succeeded. Pass Roblox’s receiptInfo table unchanged.
--!strict
local MarketplaceService = game:GetService("MarketplaceService")local ServerScriptService = game:GetService("ServerScriptService")
local DeveloperProductReceipts = require( ServerScriptService.VNCServer.Integrations.DeveloperProductReceipts)local ReceiptStatus = DeveloperProductReceipts.Status
local VNC_PROMPT_PRODUCT_IDS = { [1234567890] = true,}
local function grantProductOnce(receiptInfo): boolean -- Check PurchaseId in your durable receipt ledger. -- Grant the product exactly once, then persist that result. -- Roblox may retry after VNC asks for more time, so return true for both a -- new successful grant and a PurchaseId your ledger already granted. return trueend
MarketplaceService.ProcessReceipt = function(receiptInfo) if grantProductOnce(receiptInfo) ~= true then return Enum.ProductPurchaseDecision.NotProcessedYet end
-- Route products that are not used by VNC Prompt Purchase nodes through -- the rest of your experience's normal receipt workflow. if VNC_PROMPT_PRODUCT_IDS[receiptInfo.ProductId] ~= true then return Enum.ProductPurchaseDecision.PurchaseGranted end
local report = DeveloperProductReceipts.ReportGranted(receiptInfo) if report.status == ReceiptStatus.RuntimeUnavailable or report.status == ReceiptStatus.DataUnavailable or report.status == ReceiptStatus.AlreadyProcessing or report.status == ReceiptStatus.ResolutionFailed or report.status == ReceiptStatus.InternalError then -- The product grant is already idempotent. Ask Roblox to redeliver the -- receipt so VNC can finish its story checkpoint when it is ready. return Enum.ProductPurchaseDecision.NotProcessedYet elseif report.status ~= ReceiptStatus.Resolved and report.status ~= ReceiptStatus.Duplicate then warn("VNC did not resolve a waiting purchase node:", report.status) end
return Enum.ProductPurchaseDecision.PurchaseGrantedendReportGranted() returns a typed status from the frozen
DeveloperProductReceipts.Status catalog. Compare against those named values
instead of repeating their underlying diagnostic strings.
Only call ReportGranted for Developer Product IDs that your VNC Prompt Purchase nodes use. Your experience still grants every product and owns the durable grant ledger. For a VNC product, the example delays PurchaseGranted while the story checkpoint has a retryable failure; Roblox can then redeliver the receipt. Because the product may already have been granted, grantProductOnce must treat an already-granted PurchaseId as success without granting it twice.
Once your callback returns PurchaseGranted, do not expect Roblox to deliver that receipt again. If your receipt architecture must acknowledge Roblox before VNC is ready, use your own durable story-report retry queue instead of the NotProcessedYet strategy shown above.
Report results
Section titled “Report results”| Status | Meaning |
|---|---|
Status.Resolved | A matching waiting Developer Product node followed Purchased. |
Status.Duplicate | VNC already handled this PurchaseId; no story transition ran again. |
Status.Unmatched | The player has no matching active VNC prompt. No session is created or resurrected; when the player profile is available, VNC records the receipt so it cannot satisfy a later prompt. |
Status.InvalidReceipt | PurchaseId, PlayerId, or ProductId is missing or invalid. |
Status.DataUnavailable | The player’s VNC save profile is not available, so the report failed closed. |
Status.RuntimeUnavailable | The installed VNC runtime is not ready. |
Status.AlreadyProcessing | The matching prompt is already resolving this receipt report. |
Status.ResolutionFailed | The receipt matched, but the authored story route could not resolve. |
Status.InternalError | The VNC receipt integration failed unexpectedly and logged the underlying error. |
VNC records handled PurchaseId values in server-side player save data. This prevents the same Roblox retry from satisfying a later prompt for the same repeatable product. The ledger is not sent to runtime clients.
Failure and late-receipt behavior
Section titled “Failure and late-receipt behavior”- Prompt cancellation or a prompt API failure follows Cancelled.
- A prompt that closes positively but never receives a receipt never follows Purchased. VNC does not invent a timeout that pretends payment succeeded or failed.
- A receipt can arrive before or after the prompt-close event. It resolves the node when the player, product, active session, and receipt all match.
- A receipt reported after the player leaves or after that session is replaced is unmatched and cannot resurrect old story state.
- While a receipt is pending, VNC preserves the last playable autosave and rejects manual saves without replacing their existing slots. Rejoining and choosing Continue rolls back to that checkpoint; it does not recreate the departed purchase prompt.
- Roblox can retry the same receipt and can invoke receipt processing on more than one server. Keep your experience’s grant ledger authoritative even though VNC also deduplicates its own story transition.
Use a published private test place to verify real receipts, durable grants, rejoin behavior, and duplicate delivery. Studio prompt behavior alone cannot prove the backend receipt path.