Skip to content

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:

  1. The Roblox purchase prompt closes.
  2. 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.

The installed runtime exposes this server-only ModuleScript:

ServerScriptService.VNCServer.Integrations.DeveloperProductReceipts

Call 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 true
end
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.PurchaseGranted
end

ReportGranted() 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.

StatusMeaning
Status.ResolvedA matching waiting Developer Product node followed Purchased.
Status.DuplicateVNC already handled this PurchaseId; no story transition ran again.
Status.UnmatchedThe 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.InvalidReceiptPurchaseId, PlayerId, or ProductId is missing or invalid.
Status.DataUnavailableThe player’s VNC save profile is not available, so the report failed closed.
Status.RuntimeUnavailableThe installed VNC runtime is not ready.
Status.AlreadyProcessingThe matching prompt is already resolving this receipt report.
Status.ResolutionFailedThe receipt matched, but the authored story route could not resolve.
Status.InternalErrorThe 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.

  • 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.