Make your first API call Official guide
Make your first request to the Enviglo API, then learn how it reports errors, splits long lists into pages and limits requests.
Updated last monthby @enviglo· 5 revisions· 28 views
Enviglo's API lets your game, bot or own tools work with your store: record purchases, give products, check who owns what, run codes, read customers and more. This guide gets you from a new key to a working request, and covers what you need to know before building on it.
Before you start
You need an API key for your store. See API keys. Keep it on a server: never in a web page, a LocalScript or a public repository.
The basics
- Base URL: every address starts with
https://enviglo.com/api/v1. The API keys card in Settings → Integrations shows it too. - Your key: send it with every request as
Authorization: Bearer YOUR_API_KEY, or in anx-api-keyheader. - JSON: answers are JSON, errors included. When you send a body, send JSON with
Content-Type: application/json. - One store: a key only ever sees the store it belongs to.
- IDs are strings: Roblox and Discord IDs too, because they're too big for some languages' numbers. A Roblox ID sent as a number is taken as the same thing.
- New fields: answers can gain fields as the API grows, so ignore any you don't recognise.
Make your first call
GET /store answers with the store your key belongs to and the scopes the key has. Any key can call it, so it's the quickest way to check a key works:
curl "https://enviglo.com/api/v1/store" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"store": {
"id": "cls9x0",
"slug": "your-store",
"name": "Your Store",
"badges": ["VERIFIED"],
"robloxPlaceId": "1234567890",
"robloxGroupId": "3301",
"discordGuildId": "998877665544332211",
"_count": { "products": 12, "licenses": 3410, "followers": 220 },
"url": "/s/your-store"
},
"scopes": ["products:read", "licenses:read", "licenses:write", "discord:read"]
}
_count is the store's published products, active licenses and followers.
The same call from a Roblox server script:
-- A Script in ServerScriptService. Server only: it holds your API key.
local HttpService = game:GetService("HttpService")
local sent, response = pcall(function()
return HttpService:RequestAsync({
Url = "https://enviglo.com/api/v1/store",
Method = "GET",
Headers = { ["Authorization"] = "Bearer YOUR_API_KEY" },
})
end)
if not sent then
warn("Couldn't reach Enviglo:", response)
elseif not response.Success then
warn("Enviglo answered HTTP " .. response.StatusCode .. ": " .. response.Body)
else
local data = HttpService:JSONDecode(response.Body)
print("This key belongs to " .. data.store.name)
end
Roblox needs Allow HTTP Requests switched on for this, under Game Settings → Security in Studio.
List your products
With the Read products scope, GET /products lists your published products, sorted by name, with the Roblox and Discord IDs a game or bot needs:
curl "https://enviglo.com/api/v1/products" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"products": [
{
"id": "clx7a1b2c",
"slug": "advanced-admin",
"name": "Advanced Admin",
"status": "PUBLISHED",
"licenseType": "SINGLE",
"robuxPrice": 400,
"stock": null,
"robloxProductId": "1234567890",
"robloxGamePassId": null,
"discordRoleId": null,
"activeLicenses": 318,
"url": "/s/your-store/advanced-admin"
}
],
"excluded": { "draft": 2, "archived": 0, "suspended": 0 }
}
That's trimmed: each product carries more fields. Only published products are listed unless you ask, and excluded counts the ones left out. Add ?status=draft, ?status=archived or ?status=all to see them. A product's id is what other endpoints mean by productId.
Give someone a product
With the Grant and revoke licenses scope, POST /licenses gives someone one of your products, the same as Grant license in your dashboard. Name the product with productId, and the person with exactly one of these:
| Field | Who it names |
|---|---|
username | Their Enviglo username. |
customer | A customer, by the customer key from GET /customers. |
discordId | Their Discord user ID. |
email | Their email address. |
robloxId | Their Roblox user ID. On its own, this records a purchase made in your game. Send "source": "GRANT" with it to give instead. |
Except with robloxId, you can also send:
days: for a subscription, how many days to give, from 1 to 3,650. The product's own length unless you say.note: a private note of up to 500 characters, kept in the license's history.
Giving by robloxId can't take days or note: they're refused rather than ignored, so you never find the product's own days were given instead of yours. To give a Roblox player days or a note, name them by the customer key from GET /customers. A purchase recorded from your game doesn't use them, so a game that sends them keeps working.
To make giving safe to repeat, send a receiptId of your own with it, like an order number. The same receiptId sent again gives nothing more, and answers 200 with "created": false and the license from the first time.
curl -X POST "https://enviglo.com/api/v1/licenses" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "email": "buyer@example.com", "productId": "clx7a1b2c", "note": "Giveaway winner" }'
To give by Roblox ID, the body is:
{ "robloxId": "99999999001", "productId": "clx7a1b2c", "source": "GRANT" }
It answers 201 with "created": true and the license, or 200 with "renewed": true when it added days to a subscription they already hold. "added": true means it's another copy of a product they can hold more than one of. Every answer from POST /licenses has the same fields, created, added, renewed, preorder and banned, so your code can read them the same way whatever happened. What a grant gives depends on the product's license type, as in the dashboard: see Grant a license.
Someone who hasn't joined yet
If the Discord or Roblox account is linked on Enviglo, or someone has confirmed that email address there, the product goes straight to their account. Anyone else has it waiting for them, and shows up under Customers in your dashboard like any other customer:
- Discord ID: if the product has a Discord role and they're in your server, the role is theirs straight away. The product is theirs once they sign in to Enviglo with that Discord account, or link it to an account they already have.
- Email address: it's theirs once they sign in with that address and it's confirmed. An account that has the address but hasn't confirmed it doesn't get it. Enviglo never shows the address back to your store.
- Roblox ID: your game can check it by that Roblox ID straight away, and it's theirs once they link that Roblox account.
When a bot asks GET /discord/entitlements?discordUserId= which roles a member should have, the answer counts what you gave their Discord ID too, with "linked": false until they sign in with it or link it.
Only a robloxId can record a Roblox purchase, so "source": "ROBLOX_PURCHASE" with anyone else is refused. So is sending none of the five, or more than one: "Pass exactly one of robloxId, discordId, email, username or customer."
Read errors
When a request goes wrong, the API answers with a status code and a JSON body saying what happened:
{ "error": "This key does not have the licenses:write scope." }
| Status | What it means | For example |
|---|---|---|
| 400 | Something in the request is wrong: a missing or malformed parameter, a field the endpoint doesn't take, or a body that isn't JSON. | "robloxId must be a Roblox user ID: digits only." |
| 401 | The key is missing, mistyped or revoked. | "Invalid or revoked API key." |
| 403 | The key doesn't have the scope this needs, the store isn't open, or the store's plan doesn't include it. | "This key does not have the licenses:write scope." |
| 404 | It isn't in this store, or there's no such endpoint. A key only ever sees its own store. | "Product not found in this store." |
| 409 | It can't happen as things stand, like publishing a product that isn't ready. | "Not ready to publish: it needs a description." |
| 429 | Over a rate limit. The message names it. | "Over the limit for recording purchases made in game (300 a minute per key). Try again in 23 seconds." |
| 500 | Something went wrong at Enviglo's end. | "Something went wrong at Enviglo's end. Try again in a moment, and contact support if it keeps happening." |
Sending the same request again won't change a 400, 401, 403, 404 or 409, so fix the request first. A 429 or a 500 is worth trying again after a wait.
Pages
Lists that can run long come a page at a time: licenses, codes and requests. Ask for up to 100 at once with limit; you get 50 if you don't ask. When there's more, the answer carries nextCursor. Send it back as cursor for the next page. A nextCursor of null means you have everything.
curl "https://enviglo.com/api/v1/licenses?status=active&limit=100" \
-H "Authorization: Bearer YOUR_API_KEY"
curl "https://enviglo.com/api/v1/licenses?status=active&limit=100&cursor=clx8f2k0a0001" \
-H "Authorization: Bearer YOUR_API_KEY"
Licenses and codes come newest first, and requests most recently updated first, so each page moves on to older items.
Customers are paged by number instead: page=1, page=2 and on, with nextPage in the answer saying what's next, or null when there isn't one, and total saying how many customers there are.
Rate limits
Every key can make 1,200 requests a minute, on every plan. A few endpoints that write or add up a lot have a limit of their own on top. There are no daily or monthly quotas: each limit counts over a minute or an hour, from your first request in it, and then starts again.
| What | Endpoint | Limit |
|---|---|---|
| Every request | Every endpoint | 1,200 a minute per key |
| Recording purchases made in game | POST /licenses with a robloxId | 300 a minute per key |
| Giving, revoking, restoring and transferring licenses | POST /licenses (giving), DELETE /licenses/:id, POST /licenses/:id/restore, POST /licenses/:id/transfer | 120 a minute per store |
| Changes to any one license | DELETE /licenses/:id, POST /licenses/:id/restore, POST /licenses/:id/transfer | 10 an hour per license |
| Changing products | PATCH /products/:idOrSlug | 30 a minute per store |
| Customer lists and stats | GET /customers and GET /stats, between them | 60 a minute per key |
| Making codes | POST /codes | 60 an hour per store |
| Leak protection checks, which need no key | POST /protection/check | 600 a minute per address |
A limit per store is shared by all of your store's keys, so making more keys doesn't raise it. Your plan changes how many keys a store can have, not how much each one can do.
Every answer carries three headers saying where the key stands:
| Header | What it holds |
|---|---|
RateLimit-Limit | The key's limit: 1,200. |
RateLimit-Remaining | How many requests are left in this minute. |
RateLimit-Reset | How many seconds until the count starts again. |
Over any limit, the answer is 429. Its message names the limit, and the Retry-After header and the retryAfter field in the body say how many seconds to wait. Wait that long, then carry on as normal: nothing is held against the key. On a 429 the three headers describe the limit you went over.
{
"error": "Over the limit for recording purchases made in game (300 a minute per key). Try again in 23 seconds.",
"retryAfter": 23
}
In ProcessReceipt, don't wait: return NotProcessedYet and Roblox offers the receipt again later. Anywhere else, this waits when Enviglo says so and tries once more:
-- Sends a request, and if Enviglo says to wait (429), waits as long as it says and tries once more.
local HttpService = game:GetService("HttpService")
local function send(options)
local sent, response = pcall(HttpService.RequestAsync, HttpService, options)
if sent and response.StatusCode == 429 then
local decoded, body = pcall(HttpService.JSONDecode, HttpService, response.Body)
local wait = decoded and tonumber(body.retryAfter) or 5
task.wait(math.min(wait, 60))
sent, response = pcall(HttpService.RequestAsync, HttpService, options)
end
return sent, response
end
The lookups a game makes when players join are well inside the limits. Ask once per player per server and keep the answer, rather than asking on a loop.
A revoked or mistyped key gets 401 "Invalid or revoked API key." every time.
Next steps
- Sell with developer products: record purchases from your game.
- Check licenses in your game: unlock what players own.
- Webhooks: hear about changes in your store as they happen.
- The API reference lists every endpoint, with its scope, parameters and an example answer.