Saved Sep 28, 2026 by @enviglo: API rate limits, the answers the API gives, and codes following the dashboard's rules
"product": { "id": "clx7a1b2c", "name": "Advanced Admin", "licenseType": "SUBSCRIPTION" },"license": {"id": "clx8f2k0a0001",+ "status": "ACTIVE","source": "ROBLOX_PURCHASE","quantity": 1,"expiresAt": "2026-10-18T04:12:09.000Z",- "createdAt": "2026-09-18T04:12:09.000Z"+ "createdAt": "2026-09-18T04:12:09.000Z",+ "revokedAt": null,+ "owner": { "customer": "u_4kq8", "userId": "u_4kq8", "username": "aurora", "robloxId": "1002" },+ "product": { "id": "clx7a1b2c", "slug": "advanced-admin", "name": "Advanced Admin", "licenseType": "SUBSCRIPTION", "robloxProductId": "1234567890", "robloxGamePassId": null, "discordRoleId": null }}}```- - `owned`: whether they have it right now. A revoked license or a subscription that has run out doesn't count.+ - `owned`: whether they have it right now. A revoked license, a pre-order 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.+ - `license`: their live license, or `null` if they have none or you've banned them. It's the same license object every endpoint returns. `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. [What values mean](/developers#values) lists them all.## A server script…### 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:+ If you sell several products, one call can fetch them all, and it counts as one request against your limit however many products there are. `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.…## Caching and limits- - The check has no fixed rate limit and no daily quota. Ask once per player per server, not on a loop, and keep the answer while they're in the server, as the script does. See [API quickstart](/wiki/api-quickstart#rate-limits).+ - Each key can make 1,200 requests a minute, with no daily quota. Ask once per player per server, not on a loop, and keep the answer while they're in the server, as the script does. For several products, one `GET /api/v1/licenses?robloxId=` is one request where a check per product is several. See [Rate limits](/wiki/api-quickstart#rate-limits).- 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.+ - **400 "robloxId must be a Roblox user ID: digits only."** Send the player's `UserId`.+ - **400 "Pass productId or robloxProductId."** Send one of the two to say which product.+ - **404 "No product in this store has that productId."** The ID is wrong, or belongs to another store. A key only sees its own store.+ - **429 "Over the limit for every request (1,200 a minute per key)."** Something asks too often, usually a loop. Keep each player's answer instead of asking again.- **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.…
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, a pre-order 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 or you've banned them. It's the same license object every endpoint returns. 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. What values mean lists them all.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, and it counts as one request against your limit however many products there are. 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.
GET /api/v1/licenses?robloxId= is one request where a check per product is several. See Rate limits.UserId.