Saved Sep 19, 2026 by @enviglo: First version
+ Your game can ask Enviglo whether a player owns one of your products, and unlock it if they do. This guide is for sellers who script their own games. The check covers everything a player owns from your store, however they got it, as long as it's on the Enviglo account their Roblox account is linked to.+ ## Before you start++ - You need an API key with the **Check licenses** (`licenses:read`) scope. New keys have it ticked. See [API keys](/wiki/api-keys). The in-game hub's key won't do: it has no API scopes.+ - Your game needs **Allow HTTP Requests** switched on, under **Game Settings** → **Security** in Studio.+ - You need the product's Enviglo ID, or its developer product ID. `GET /api/v1/products` lists both, as `id` and `robloxProductId`.++ ## The request++ Send a `GET` to `https://enviglo.com/api/v1/licenses/check` with your key and these query parameters:++ | Parameter | What it is |+ |---|---|+ | `robloxId` | Required. The player's Roblox user ID. |+ | `productId` | The product's Enviglo ID. Send this or `robloxProductId`. |+ | `robloxProductId` | The product's developer product ID. |++ To try it outside Roblox:++ ```bash+ curl "https://enviglo.com/api/v1/licenses/check?robloxId=1002&robloxProductId=1234567890" \+ -H "Authorization: Bearer YOUR_API_KEY"+ ```++ ## The answer++ ```json+ {+ "owned": true,+ "banned": false,+ "copies": 1,+ "product": { "id": "clx7a1b2c", "name": "Advanced Admin", "licenseType": "SUBSCRIPTION" },+ "license": {+ "id": "clx8f2k0a0001",+ "source": "ROBLOX_PURCHASE",+ "quantity": 1,+ "expiresAt": "2026-10-18T04:12:09.000Z",+ "createdAt": "2026-09-18T04:12:09.000Z"+ }+ }+ ```++ - `owned`: whether they have it right now. A revoked license or a subscription that has run out doesn't count.+ - `banned`: whether you've banned this player, or the Enviglo account they linked. A banned player never owns anything from your store, so check this before offering a purchase.+ - `copies`: how many they hold. Every purchase of a **Buy more than once** product is a license of its own, and this counts them. It's 0 for a banned player.+ - `product`: the product's `id`, `name` and `licenseType`. The license types are `SINGLE`, `MULTIPLE` and `SUBSCRIPTION`: **One purchase**, **Buy more than once** and **Subscription** in the dashboard.+ - `license`: their live license, or `null` if they have none. `source` says how they got it, such as `ROBLOX_PURCHASE`, `GRANT` or `TRANSFER`. `expiresAt` is when a subscription runs out, and `null` for everything else.++ ## A server script++ This script checks one product for each player who joins, and keeps the answer while they're in the server. Put it in a Script in ServerScriptService, and fill in your key and the product's Enviglo ID.++ ```lua+ -- EnvigloOwnership: 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 Players = game:GetService("Players")++ local API_URL = "https://enviglo.com/api/v1"+ local API_KEY = "YOUR_API_KEY" -- a key with the licenses:read scope+ local PRODUCT_ID = "YOUR_PRODUCT_ID" -- the product's id from GET /api/v1/products++ -- Each player's answer, kept while they're in this server.+ local answers = {}++ -- Asks Enviglo whether a player owns the product. Returns the answer, or nil if it couldn't ask.+ local function checkLicense(player)+ local url = API_URL .. "/licenses/check?robloxId=" .. tostring(player.UserId)+ .. "&productId=" .. HttpService:UrlEncode(PRODUCT_ID)+ local sent, response = pcall(function()+ return HttpService:RequestAsync({+ Url = url,+ Method = "GET",+ Headers = { ["Authorization"] = "Bearer " .. API_KEY },+ })+ end)+ if not sent then+ warn("Enviglo: couldn't check a license:", response)+ return nil+ end+ if not response.Success then+ warn("Enviglo: HTTP " .. response.StatusCode .. ": " .. response.Body)+ return nil+ end+ local decoded, answer = pcall(function()+ return HttpService:JSONDecode(response.Body)+ end)+ if not decoded then+ return nil+ end+ return answer+ end++ local function onPlayerAdded(player)+ local answer = checkLicense(player)+ if not answer then+ -- Couldn't ask. That isn't the same as "doesn't own it", so try again later.+ return+ end+ answers[player] = answer++ if answer.banned then+ print(player.Name .. " is banned from this store")+ elseif answer.owned then+ print(player.Name .. " owns " .. answer.product.name .. ", " .. answer.copies .. " held")+ if answer.license and answer.license.expiresAt then+ print("Their subscription runs until " .. answer.license.expiresAt)+ end+ -- Unlock the product for them here.+ else+ -- Offer it, or remind them to link Roblox on Enviglo if they got it there.+ print(player.Name .. " doesn't own " .. answer.product.name)+ end+ end++ Players.PlayerAdded:Connect(onPlayerAdded)+ for _, player in ipairs(Players:GetPlayers()) do+ task.spawn(onPlayerAdded, player)+ end++ Players.PlayerRemoving:Connect(function(player)+ answers[player] = nil+ end)+ ```++ ### Everything a player owns at once++ If you sell several products, one call can fetch them all. `GET /api/v1/licenses?robloxId=` answers with `banned` and a `licenses` list: every license that gives the player access right now, each with its `product`. A banned player comes back with an empty list. Add this to the script above:++ ```lua+ -- Every product a player owns in your store, as a set of Enviglo product ids.+ -- Returns nil if it couldn't ask. Uses API_URL and API_KEY from the script above.+ local function ownedProducts(player)+ local sent, response = pcall(function()+ return HttpService:RequestAsync({+ Url = API_URL .. "/licenses?robloxId=" .. tostring(player.UserId),+ Method = "GET",+ Headers = { ["Authorization"] = "Bearer " .. API_KEY },+ })+ end)+ if not sent or not response.Success then+ return nil+ end+ local decoded, data = pcall(function()+ return HttpService:JSONDecode(response.Body)+ end)+ if not decoded then+ return nil+ end+ local owned = {}+ for _, license in ipairs(data.licenses) do+ owned[license.product.id] = true+ end+ return owned+ end+ ```++ A product bought more than once appears once for each purchase. The full details are in [the API reference](/developers#player-licenses).++ ## Players who haven't linked Roblox++ Licenses belong to Enviglo accounts, and the check finds them through the account a player's Roblox account is linked to.++ - **They bought it in game.** The check finds it even if they've never visited Enviglo. The license waits on a placeholder account that belongs to their Roblox ID, and moves to their own account when they link.+ - **They got it on Enviglo.** A free product, a code, or a license your team gave to their username sits on their Enviglo account. Your game can't see it until they link the Roblox account they play on, so `owned` is `false` until then.++ If a player says they own something your game can't see, ask them to link Roblox in **Settings** → **Connections** on Enviglo. [Link your Roblox account](/wiki/link-your-roblox-account) shows them how. Once they have, the next check finds it.++ ## Caching and limits++ - The check has no fixed rate limit, but ask once per player per server, not on a loop. Keep the answer while they're in the server, as the script does.+ - When your ProcessReceipt records a purchase, update the player's saved answer, or check again, so the product unlocks straight away.+ - If a check fails, don't treat the player as owning nothing. Try again later.+ - A subscription can run out while someone plays. The next check, when they join again, says so.++ ## Troubleshooting++ - **400 "Provide a numeric robloxId."** Send the player's `UserId`.+ - **404 "Product not found in this store."** The ID is wrong, or belongs to another store. A key only sees its own store.+ - **401 "Invalid or revoked API key."** Check the key, or make a new one if it was revoked.+ - **403 "This key does not have the licenses:read scope."** Make a key with **Check licenses**.+ - **A player who bought it shows as not owning it.** They may have got it on Enviglo and not linked Roblox, or linked a different Roblox account. A subscription may have run out, or the license may have been revoked. Look them up under **Customers** in your store's dashboard.+ - **You deleted the product.** People who own it keep it, and its IDs still work with the check.
Your game can ask Enviglo whether a player owns one of your products, and unlock it if they do. This guide is for sellers who script their own games. The check covers everything a player owns from your store, however they got it, as long as it's on the Enviglo account their Roblox account is linked to.
licenses:read) scope. New keys have it ticked. See API keys. The in-game hub's key won't do: it has no API scopes.GET /api/v1/products lists both, as id and robloxProductId.Send a GET to https://enviglo.com/api/v1/licenses/check with your key and these query parameters:
| Parameter | What it is |
|---|---|
robloxId | Required. The player's Roblox user ID. |
productId | The product's Enviglo ID. Send this or robloxProductId. |
robloxProductId | The product's developer product ID. |
To try it outside Roblox:
curl "https://enviglo.com/api/v1/licenses/check?robloxId=1002&robloxProductId=1234567890" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"owned": true,
"banned": false,
"copies": 1,
"product"
owned: whether they have it right now. A revoked license or a subscription that has run out doesn't count.banned: whether you've banned this player, or the Enviglo account they linked. A banned player never owns anything from your store, so check this before offering a purchase.copies: how many they hold. Every purchase of a Buy more than once product is a license of its own, and this counts them. It's 0 for a banned player.product: the product's id, name and licenseType. The license types are SINGLE, MULTIPLE and SUBSCRIPTION: One purchase, Buy more than once and Subscription in the dashboard.license: their live license, or null if they have none. source says how they got it, such as ROBLOX_PURCHASE, GRANT or TRANSFER. expiresAt is when a subscription runs out, and null for everything else.This script checks one product for each player who joins, and keeps the answer while they're in the server. Put it in a Script in ServerScriptService, and fill in your key and the product's Enviglo ID.
-- EnvigloOwnership: 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 Players = game:GetService("Players")
local API_URL = "https://enviglo.com/api/v1"
local API_KEY = "YOUR_API_KEY" -- a key with the licenses:read scope
local PRODUCT_ID = "YOUR_PRODUCT_ID" -- the product's id from GET /api/v1/products
-- Each player's answer, kept while they're in this server.
local answers = {}
-- Asks Enviglo whether a player owns the product. Returns the answer, or nil if it couldn't ask.
local function checkLicense(player)
local url = API_URL .. "/licenses/check?robloxId=" .. tostring(player.UserId)
.. "&productId=" .. HttpService:UrlEncode(PRODUCT_ID)
local sent, response = pcall(function()
return HttpService:RequestAsync({
Url = url,
Method = "GET",
Headers = { ["Authorization"] = "Bearer " .. API_KEY },
})
end)
if not sent then
warn("Enviglo: couldn't check a license:", response)
return nil
end
if not response.Success then
warn("Enviglo: HTTP " .. response.StatusCode .. ": " .. response.Body)
return nil
end
local decoded, answer = pcall(function()
return HttpService:JSONDecode(response.Body)
end)
if not decoded then
return nil
end
return answer
end
local function onPlayerAdded(player)
local answer = checkLicense(player)
if not answer then
-- Couldn't ask. That isn't the same as "doesn't own it", so try again later.
return
end
answers[player] = answer
if answer.banned then
print(player.Name .. " is banned from this store")
elseif answer.owned then
print(player.Name .. " owns " .. answer.product.name .. ", " .. answer.copies .. " held")
if answer.license and answer.license.expiresAt then
print("Their subscription runs until " .. answer.license.expiresAt)
end
-- Unlock the product for them here.
else
-- Offer it, or remind them to link Roblox on Enviglo if they got it there.
print(player.Name .. " doesn't own " .. answer.product.name)
end
end
Players.PlayerAdded:Connect(onPlayerAdded)
for _, player in ipairs(Players:GetPlayers()) do
task.spawn(onPlayerAdded, player)
end
Players.PlayerRemoving:Connect(function(player)
answers[player] = nil
end)
If you sell several products, one call can fetch them all. GET /api/v1/licenses?robloxId= answers with banned and a licenses list: every license that gives the player access right now, each with its product. A banned player comes back with an empty list. Add this to the script above:
-- Every product a player owns in your store, as a set of Enviglo product ids.
-- Returns nil if it couldn't ask. Uses API_URL and API_KEY from the script above.
local function ownedProducts(player)
local sent, response = pcall(function()
return HttpService:RequestAsync({
Url = API_URL .. "/licenses?robloxId=" .. tostring(player.UserId),
Method = "GET",
Headers = { ["Authorization"] = "Bearer " .. API_KEY },
})
end)
if not sent or not response.Success then
return nil
end
local decoded, data = pcall(function()
return HttpService:JSONDecode(response.Body)
end)
if not decoded then
return nil
end
local owned = {}
for _, license in ipairs(data.licenses) do
owned[license.product.id] = true
end
return owned
end
A product bought more than once appears once for each purchase. The full details are in the API reference.
Licenses belong to Enviglo accounts, and the check finds them through the account a player's Roblox account is linked to.
owned is false until then.If a player says they own something your game can't see, ask them to link Roblox in Settings → Connections on Enviglo. Link your Roblox account shows them how. Once they have, the next check finds it.
UserId.