Saved Sep 19, 2026 by @enviglo: First version
+ Use this if you run your own Roblox game and want to sell your Enviglo products in your own shop. Each product is a Roblox developer product. When a player buys one, your game sends the receipt to Enviglo, which records the purchase and gives the player the license.+ ## Before you start++ - You need an API key with the **Grant and revoke licenses** (`licenses:write`) scope. New keys have it ticked if you can grant, revoke and transfer licenses yourself. See [API keys](/wiki/api-keys).+ - Your game needs **Allow HTTP Requests** switched on. In Studio, it's under **Game Settings** → **Security**.+ - The product needs to exist on Enviglo. See [Create and publish a product](/wiki/create-a-product).++ ## Create the developer product++ ### Let Enviglo create it++ Connect your Roblox experience once, and Enviglo makes a developer product whenever you give a product a Robux price.++ 1. Check your store's **Roblox place ID** is set, under **Settings** → **Store** → **Links**. Enviglo works in that place's experience.+ 2. In **Settings** → **Integrations**, connect in the **Roblox experience** card, either way:+ - **Connect with Roblox**, when the card offers it. The card says when only a limited number of stores can still connect this way.+ - **Connect with an API key**. The card lists the steps: create an Open Cloud API key in Roblox's Creator Dashboard, give it **developer-product** with Read and Write for your experience (and **game-pass** too if you sell game passes), add `0.0.0.0/0` under its accepted IP addresses, then paste it into **Open Cloud API key** and press **Connect**.+ 3. Open the product, go to **Pricing**, fill in **Price in Robux**, and press **Save pricing**.++ Enviglo creates the developer product in your experience and fills in the product's **Developer product ID**, and the confirmation says it was created on Roblox at your price. Change the price later and Roblox follows. Roblox can't delete a developer product, so one that's been made stays.++ ### Add one you made++ 1. Create a developer product for your experience on Roblox, and copy its ID.+ 2. On Enviglo, open the product and go to **Delivery**.+ 3. Paste the ID into **Developer product ID**, then press **Save delivery**.++ A developer product can belong to only one product in your store. If another product has it, or a deleted product that people still own, Enviglo says so and doesn't save.++ Roblox decides what players pay. Enviglo reads the price from Roblox and shows it on your product page. To read it again, press **Check Roblox** in the panel at the top of **Delivery**.++ ## Record purchases from ProcessReceipt++ Roblox calls your game's ProcessReceipt for each developer product purchase. Your handler sends the receipt to Enviglo with `POST /api/v1/licenses`:++ | Field | What to send | Notes |+ |---|---|---|+ | `robloxId` | `tostring(receiptInfo.PlayerId)` | Required. The buyer's Roblox user ID, as a string. |+ | `robloxProductId` | `tostring(receiptInfo.ProductId)` | The developer product, as a string. You can send `productId`, the product's Enviglo ID, instead. |+ | `receiptId` | `receiptInfo.PurchaseId` | Makes the call safe to repeat: each receipt is recorded once. |+ | `robuxSpent` | `receiptInfo.CurrencySpent` | A whole number. Shown as what was paid on the purchase. |+ | `placeId` | `tostring(receiptInfo.PlaceIdWherePurchased)` | Shown as where it was bought. |++ Return PurchaseGranted only when Enviglo answers with a success, and NotProcessedYet otherwise. Roblox then offers the receipt again later. Because Enviglo records each receipt once, a retry never records a purchase twice.++ Put this in a Script in ServerScriptService, and replace `YOUR_API_KEY` with your key:++ ```lua+ -- EnvigloReceipts: a Script in ServerScriptService.+ -- It holds your API key, so keep it a server Script. Never a LocalScript, and never in ReplicatedStorage.+ local HttpService = game:GetService("HttpService")+ local MarketplaceService = game:GetService("MarketplaceService")++ local API_URL = "https://enviglo.com/api/v1"+ local API_KEY = "YOUR_API_KEY" -- a key with the licenses:write scope++ -- Sends one receipt to Enviglo. Returns true once Enviglo has it on record.+ local function recordPurchase(receiptInfo)+ local sent, response = pcall(function()+ return HttpService:RequestAsync({+ Url = API_URL .. "/licenses",+ Method = "POST",+ Headers = {+ ["Authorization"] = "Bearer " .. API_KEY,+ ["Content-Type"] = "application/json",+ },+ Body = HttpService:JSONEncode({+ robloxId = tostring(receiptInfo.PlayerId),+ robloxProductId = tostring(receiptInfo.ProductId),+ receiptId = receiptInfo.PurchaseId,+ robuxSpent = receiptInfo.CurrencySpent,+ placeId = tostring(receiptInfo.PlaceIdWherePurchased),+ }),+ })+ end)++ if not sent then+ -- No answer at all: HTTP requests are switched off, or Enviglo couldn't be reached.+ warn("Enviglo: couldn't send the purchase:", response)+ return false+ end+ if not response.Success then+ -- Enviglo said no. The body says why, as { "error": "..." }.+ warn("Enviglo: HTTP " .. response.StatusCode .. ": " .. response.Body)+ return false+ end+ return true+ end++ MarketplaceService.ProcessReceipt = function(receiptInfo)+ if recordPurchase(receiptInfo) then+ -- Enviglo has it on record: a new license, another copy, more subscription time,+ -- or a receipt it had already recorded. Unlock anything in game here too.+ return Enum.ProductPurchaseDecision.PurchaseGranted+ end+ -- Not recorded. Roblox offers the receipt again later, and Enviglo records+ -- each PurchaseId only once, so trying again is safe.+ return Enum.ProductPurchaseDecision.NotProcessedYet+ end+ ```++ Keep your key in server scripts only. See [Keep keys safe](/wiki/api-keys#keep-keys-safe) for other ways to store it, including Roblox's own Secrets.++ ### If your game sells other developer products++ A game has only one ProcessReceipt. If you also sell things that aren't on Enviglo, send only Enviglo's developer products to the API, and handle the rest as you do now. Replace the ProcessReceipt in the script above with this:++ ```lua+ -- Developer products that belong to Enviglo products: the Developer product ID+ -- from each product's Delivery page.+ local ENVIGLO_PRODUCTS = {+ [1234567890] = true,+ [1234567891] = true,+ }++ -- Your game's own code for everything else it sells.+ local function handleOtherPurchase(receiptInfo)+ -- Give the player what they bought, then:+ return Enum.ProductPurchaseDecision.PurchaseGranted+ end++ MarketplaceService.ProcessReceipt = function(receiptInfo)+ if not ENVIGLO_PRODUCTS[receiptInfo.ProductId] then+ return handleOtherPurchase(receiptInfo)+ end+ if recordPurchase(receiptInfo) then+ return Enum.ProductPurchaseDecision.PurchaseGranted+ end+ return Enum.ProductPurchaseDecision.NotProcessedYet+ end+ ```++ You can also build that list when the server starts: `GET /api/v1/products` returns each product's `robloxProductId`, with a key that has the **Read products** scope. The API sends IDs as strings and `receiptInfo.ProductId` is a number, so key the list with `tonumber()`.++ ## What Enviglo answers++ Any success means Enviglo has the purchase on record, so your game can finish the receipt:++ | Status | In the answer | What happened |+ |---|---|---|+ | 201 | `"created": true` | A new license: a first purchase, or buying again after a revoke or transfer. |+ | 201 | `"created": true` and `"added": true` | Another license for a **Buy more than once** product. |+ | 200 | `"renewed": true` | A subscription got its days. `license.expiresAt` is the new end. |+ | 200 | `"created": false`, with no `"renewed"` | Nothing new to give: the receipt was already recorded, or the player already owns this **One purchase** product. The payment is still recorded. |+ | 200 or 201 | `"banned": true` | You've banned this player. The purchase is recorded and the license is revoked. |++ A first purchase looks like this:++ ```json+ {+ "created": true,+ "added": false,+ "renewed": false,+ "license": {+ "id": "clx8f2k0a0001",+ "source": "ROBLOX_PURCHASE",+ "status": "ACTIVE",+ "quantity": 1,+ "expiresAt": null,+ "createdAt": "2026-09-18T04:12:09.000Z"+ },+ "discordRoleId": null+ }+ ```++ `discordRoleId` is the product's Discord role, if it has one. Enviglo gives the role itself to buyers who've linked Discord, so you only need it if your own bot does something with it. See [the API reference](/developers#record-purchase) for every field.++ ## The Buy on Roblox button++ When a product has a developer product, **Buy on Roblox** on its Enviglo page opens your store's place with launch data like `enviglo:buy:1234567890`, where the number is the developer product's ID. Read it when the player joins and prompt the purchase:++ ```lua+ -- EnvigloLaunch: a Script in ServerScriptService.+ local MarketplaceService = game:GetService("MarketplaceService")+ local Players = game:GetService("Players")++ Players.PlayerAdded:Connect(function(player)+ -- "Buy on Roblox" sends launch data like "enviglo:buy:1234567890".+ local launchData = player:GetJoinData().LaunchData or ""+ local developerProductId = tonumber(string.match(launchData, "^enviglo:buy:(%d+)$"))+ if not developerProductId then+ return+ end++ -- Prompt once their character has spawned.+ if not player.Character then+ player.CharacterAdded:Wait()+ end++ -- Players can edit launch data, so it only decides what to offer.+ -- What they get is decided by the receipt in ProcessReceipt.+ local ok, err = pcall(function()+ MarketplaceService:PromptProductPurchase(player, developerProductId)+ end)+ if not ok then+ warn("Enviglo: couldn't prompt the purchase:", err)+ end+ end)+ ```++ Launch data can be edited by the player, so treat it as a suggestion of what to offer, never as proof of anything. What they get is decided by the receipt.++ The button sends buyers to your store's **Roblox place ID** (**Settings** → **Store** → **Links**), or to **Place ID for this product** on the product's **Delivery** page if you've set one. With neither, the product page shows **Sold in-game** instead of the button.++ ## Troubleshooting++ - **The request fails before it reaches Enviglo.** Usually HTTP requests are off. Turn on **Allow HTTP Requests** under **Game Settings** → **Security**.+ - **401 "Invalid or revoked API key."** Check the key was copied in full. If it was revoked, make a new one.+ - **403 "This key does not have the licenses:write scope."** Make a key with **Grant and revoke licenses**.+ - **404 "Product not found in this store. Pass productId or robloxProductId."** No product in your store has this developer product. Check **Developer product ID** on the product's **Delivery** page. The script leaves the receipt waiting, so it's recorded the next time Roblox offers it after you fix the ID.+ - **400 with a message about the body**, such as "robloxId must be numeric". Check the fields against the table above: IDs as strings with `tostring()`, and `robuxSpent` as a whole number.+ - **409 "A license for this player and product could not be recorded. Retry the purchase later."** Two requests raced. Returning NotProcessedYet lets Roblox try again.+ - **429 "Too many licenses recorded in the last minute. Wait a moment and retry."** A key can record 300 purchases a minute. Returning NotProcessedYet lets Roblox try again.+ - **Testing in Studio.** Test purchases in Studio go through ProcessReceipt too, and Enviglo records them like any other. Purchase records are permanent, so test with care, and revoke the test license afterwards.+ - **Stock.** Stock counts down when a purchase gives a player something, but it never blocks a purchase, because the player has already paid. If you use stock, check it before you prompt: `GET /api/v1/products` returns each product's `stock`.
Use this if you run your own Roblox game and want to sell your Enviglo products in your own shop. Each product is a Roblox developer product. When a player buys one, your game sends the receipt to Enviglo, which records the purchase and gives the player the license.
licenses:write) scope. New keys have it ticked if you can grant, revoke and transfer licenses yourself. See API keys.Connect your Roblox experience once, and Enviglo makes a developer product whenever you give a product a Robux price.
0.0.0.0/0 under its accepted IP addresses, then paste it into Open Cloud API key and press Connect.Enviglo creates the developer product in your experience and fills in the product's Developer product ID, and the confirmation says it was created on Roblox at your price. Change the price later and Roblox follows. Roblox can't delete a developer product, so one that's been made stays.
A developer product can belong to only one product in your store. If another product has it, or a deleted product that people still own, Enviglo says so and doesn't save.
Roblox decides what players pay. Enviglo reads the price from Roblox and shows it on your product page. To read it again, press Check Roblox in the panel at the top of Delivery.
Roblox calls your game's ProcessReceipt for each developer product purchase. Your handler sends the receipt to Enviglo with POST /api/v1/licenses:
| Field | What to send | Notes |
|---|---|---|
robloxId | tostring(receiptInfo.PlayerId) | Required. The buyer's Roblox user ID, as a string. |
robloxProductId | tostring(receiptInfo.ProductId) | The developer product, as a string. You can send productId, the product's Enviglo ID, instead. |
receiptId | receiptInfo.PurchaseId | Makes the call safe to repeat: each receipt is recorded once. |
robuxSpent | receiptInfo.CurrencySpent | A whole number. Shown as what was paid on the purchase. |
placeId | tostring(receiptInfo.PlaceIdWherePurchased) | Shown as where it was bought. |
Return PurchaseGranted only when Enviglo answers with a success, and NotProcessedYet otherwise. Roblox then offers the receipt again later. Because Enviglo records each receipt once, a retry never records a purchase twice.
Put this in a Script in ServerScriptService, and replace YOUR_API_KEY with your key:
-- EnvigloReceipts: a Script in ServerScriptService.
-- It holds your API key, so keep it a server Script. Never a LocalScript, and never in ReplicatedStorage.
local HttpService = game:GetService("HttpService")
local MarketplaceService = game:GetService("MarketplaceService")
local API_URL = "https://enviglo.com/api/v1"
local API_KEY = "YOUR_API_KEY" -- a key with the licenses:write scope
-- Sends one receipt to Enviglo. Returns true once Enviglo has it on record.
local function recordPurchase(receiptInfo)
local sent, response = pcall(function()
return HttpService:RequestAsync({
Url = API_URL .. "/licenses",
Method = "POST",
Headers = {
["Authorization"] = "Bearer " .. API_KEY,
["Content-Type"] = "application/json",
},
Body = HttpService:JSONEncode({
robloxId = tostring(receiptInfo.PlayerId),
robloxProductId = tostring(receiptInfo.ProductId),
receiptId = receiptInfo.PurchaseId,
robuxSpent = receiptInfo.CurrencySpent,
placeId = tostring(receiptInfo.PlaceIdWherePurchased),
}),
})
end)
if not sent then
-- No answer at all: HTTP requests are switched off, or Enviglo couldn't be reached.
warn("Enviglo: couldn't send the purchase:", response)
return false
end
if not response.Success then
-- Enviglo said no. The body says why, as { "error": "..." }.
warn("Enviglo: HTTP " .. response.StatusCode .. ": " .. response.Body)
return false
end
return true
end
MarketplaceService.ProcessReceipt = function(receiptInfo)
if recordPurchase(receiptInfo) then
-- Enviglo has it on record: a new license, another copy, more subscription time,
-- or a receipt it had already recorded. Unlock anything in game here too.
return Enum.ProductPurchaseDecision.PurchaseGranted
end
-- Not recorded. Roblox offers the receipt again later, and Enviglo records
-- each PurchaseId only once, so trying again is safe.
return Enum.ProductPurchaseDecision.NotProcessedYet
end
Keep your key in server scripts only. See Keep keys safe for other ways to store it, including Roblox's own Secrets.
A game has only one ProcessReceipt. If you also sell things that aren't on Enviglo, send only Enviglo's developer products to the API, and handle the rest as you do now. Replace the ProcessReceipt in the script above with this:
-- Developer products that belong to Enviglo products: the Developer product ID
-- from each product's Delivery page.
local ENVIGLO_PRODUCTS = {
[1234567890] = true,
[1234567891] = true,
}
-- Your game's own code for everything else it sells.
local function handleOtherPurchase(receiptInfo)
-- Give the player what they bought, then:
return Enum.ProductPurchaseDecision.PurchaseGranted
end
MarketplaceService.ProcessReceipt = function(receiptInfo)
if not ENVIGLO_PRODUCTS[receiptInfo.ProductId] then
return handleOtherPurchase(receiptInfo)
end
if recordPurchase(receiptInfo) then
return Enum.ProductPurchaseDecision.PurchaseGranted
end
return Enum.ProductPurchaseDecision.NotProcessedYet
end
You can also build that list when the server starts: GET /api/v1/products returns each product's robloxProductId, with a key that has the Read products scope. The API sends IDs as strings and receiptInfo.ProductId is a number, so key the list with tonumber().
Any success means Enviglo has the purchase on record, so your game can finish the receipt:
| Status | In the answer | What happened |
|---|---|---|
| 201 | "created": true | A new license: a first purchase, or buying again after a revoke or transfer. |
| 201 | "created": true and "added": true | Another license for a Buy more than once product. |
| 200 | "renewed": true | A subscription got its days. license.expiresAt is the new end. |
| 200 | "created": false, with no "renewed" | Nothing new to give: the receipt was already recorded, or the player already owns this One purchase product. The payment is still recorded. |
| 200 or 201 | "banned": true | You've banned this player. The purchase is recorded and the license is revoked. |
A first purchase looks like this:
{
"created": true,
"added": false,
"renewed": false,
"license": {
"id": "clx8f2k0a0001",
"source": "ROBLOX_PURCHASE",
"status": "ACTIVE",
"quantity": 1,
"expiresAt": null,
"createdAt": "2026-09-18T04:12:09.000Z"
},
"discordRoleId": null
}
discordRoleId is the product's Discord role, if it has one. Enviglo gives the role itself to buyers who've linked Discord, so you only need it if your own bot does something with it. See the API reference for every field.
When a product has a developer product, Buy on Roblox on its Enviglo page opens your store's place with launch data like enviglo:buy:1234567890, where the number is the developer product's ID. Read it when the player joins and prompt the purchase:
-- EnvigloLaunch: a Script in ServerScriptService.
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(player)
-- "Buy on Roblox" sends launch data like "enviglo:buy:1234567890".
local launchData = player:GetJoinData().LaunchData or ""
local developerProductId = tonumber(string.match(launchData, "^enviglo:buy:(%d+)$"))
if not developerProductId then
return
end
-- Prompt once their character has spawned.
if not player.Character then
player.CharacterAdded:Wait()
end
-- Players can edit launch data, so it only decides what to offer.
-- What they get is decided by the receipt in ProcessReceipt.
local ok, err = pcall(function()
MarketplaceService:PromptProductPurchase(player, developerProductId)
end)
if not ok then
warn("Enviglo: couldn't prompt the purchase:", err)
end
end)
Launch data can be edited by the player, so treat it as a suggestion of what to offer, never as proof of anything. What they get is decided by the receipt.
The button sends buyers to your store's Roblox place ID (Settings → Store → Links), or to Place ID for this product on the product's Delivery page if you've set one. With neither, the product page shows Sold in-game instead of the button.
tostring(), and robuxSpent as a whole number.GET /api/v1/products returns each product's stock.