Developer documentation

Build with LicenseManager without guessing.

Check product ownership, create orders, and integrate license verification into your own applications using the API your account already exposes.

Base URLhttps://YOUR_DOMAIN
Loading… This value comes from the server BASE_URL environment variable.
License checksVerify a Roblox player's ownership.
OrdersCreate and poll QR payments.
WebhooksConfirm payment callbacks safely.

Authentication

Use an API key for application requests. Dashboard-only endpoints use the authenticated Supabase session.

KindHeaderUsed for
API keyX-API-Key: lm_…/api/check and everything under /api/v1
Dashboard loginAuthorization: Bearer <token>/api/keys and /api/settings (used by the dashboard)
!
Keep API keys server-side.Never put one in a LocalScript, a client-visible module, or a public repository.

An API key is lm_ followed by 40 lowercase hex characters. A key only reaches the products of the account that created it, and a revoked key stops working immediately.

Scopes & rate limits

Every API key carries scopes. A request outside the key's scopes fails with 403 insufficient_scope. Pick the narrowest access that works when you create a key in the dashboard.

ScopeAllowsLimit
checkGET /api/check120 per minute
productsGET /api/v1/products60 per minute
ordersPOST and GET /api/v1/orders60 per minute
whitelist:readGET /api/v1/whitelist60 per minute
whitelist:writePOST and DELETE /api/v1/whitelist60 per minute

Dashboard presets: Check only holds just check, made for Roblox scripts. Full access holds every scope, for your bot or storefront server. Keys created before scopes existed keep full access.

!
Use a check-only key inside Roblox.If that key ever leaks, the worst case is someone learning whether a player owns a product. They cannot create orders or edit the whitelist.

Limits are counted per key and per scope. Every response from a limited endpoint carries X-RateLimit-Limit and X-RateLimit-Remaining. Past the limit you get 429 rate_limited with a Retry-After header, in seconds. Limits are best effort: each server instance counts on its own, so treat them as protection against floods, not exact quotas. Cache results on your side and check once per player join.

Errors

Errors use a compact JSON shape so integrations can handle them consistently.

StatusCodeMeaning
401unauthorizedKey is missing, malformed, unknown or revoked
403insufficient_scopeThe key is valid but lacks the scope this endpoint needs. Response includes required
429rate_limitedToo many requests. Wait retry_after seconds
405method_not_allowedWrong HTTP method
500server_not_configuredThe server is missing environment variables
500internal_errorUnexpected crash. Check the server logs

API reference

Each endpoint below includes the authentication method, parameters, examples, and the response your client should expect.

GET/api/checkAPI key

Tells you whether a player is on a product's whitelist. Every check is also saved to your logs.

Scope check · 120 requests per minute per key.

ParameterRequiredRules
robloxUserIdyesDigits only, up to 15
productyesProduct slug, like my-product
usernamenoLetters, digits, underscore, up to 20. Only used in the log
curl "https://YOUR_DOMAIN/api/check?robloxUserId=123456&product=my-product" \
  -H "X-API-Key: lm_your_key_here"
200 OK
{ "owned": true }

Other responses: 400 bad_request for invalid input, 401 unauthorized, 403 insufficient_scope, 429 rate_limited, 502 lookup_failed for a database error.

GETRoblox ServerScriptExample

Put this in a Script inside ServerScriptService, and turn on Allow HTTP Requests in Game Settings.

local HttpService = game:GetService("HttpService")
local Players = game:GetService("Players")

local BASE = "https://YOUR_DOMAIN"
local API_KEY = "lm_your_key_here"
local PRODUCT = "my-product"

local function owns(player)
    local url = string.format("%s/api/check?robloxUserId=%d&product=%s&username=%s",
        BASE, player.UserId, PRODUCT, player.Name)
    local ok, res = pcall(function()
        return HttpService:RequestAsync({
            Url = url,
            Method = "GET",
            Headers = { ["X-API-Key"] = API_KEY },
        })
    end)
    if not ok or not res.Success then
        warn("License check failed:", ok and res.StatusCode or res)
        return false -- denies access when the check fails
    end
    return HttpService:JSONDecode(res.Body).owned == true
end

Players.PlayerAdded:Connect(function(player)
    if not owns(player) then
        player:Kick("You do not own this product.")
    end
end)
GET/api/v1/productsAPI key

Returns the products of the key's owner. Prices are whole numbers. Scope products.

{ "products": [ { "name": "My Product", "slug": "my-product", "price": 15000 } ] }
POST/api/v1/ordersAPI key

Scope orders. Starts a payment in the owner's Tokoshopp account. The owner must save a Tokoshopp key in the dashboard's Payments tab first, and the product price must be at least 1000.

FieldRequiredRules
productyesProduct slug
roblox_user_idyesDigits only, up to 15
buyer_refnoYour own reference, cut to 64 characters
curl -X POST "https://YOUR_DOMAIN/api/v1/orders" \
  -H "X-API-Key: lm_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"product":"my-product","roblox_user_id":"123456"}'
201 Created
{
  "order_id": "uuid",
  "transaction_id": "TX123",
  "status": "pending",
  "product": "My Product",
  "amount": 15000,
  "total": 15012,
  "expires_at": "2026-10-01T12:30:00Z",
  "qr_image": "https://…",
  "qr_string": "000201…"
}

total is what the buyer pays, as returned by Tokoshopp. Show qr_image to the buyer.

StatusCodeCause
400bad_requestInvalid slug or user ID
400product_not_for_salePrice is below 1000
400payment_not_configuredNo Tokoshopp key saved
404product_not_foundNo such slug for this owner
409already_ownedThe player is already whitelisted
429too_many_pending_ordersThe player has 3 unpaid orders open
502payment_provider_errorTokoshopp refused the payment. May include message
500order_sync_failedPayment exists at Tokoshopp but was not saved. The response has order_id and transaction_id so you can reconcile it
GET/api/v1/orders?id=ORDER_IDAPI key

Scope orders. Returns the order. While it is pending, the server asks Tokoshopp for the real status first, so polling this endpoint is enough to finish a purchase. When it becomes paid, the player is added to the whitelist automatically.

{
  "order_id": "uuid",
  "status": "paid",
  "product": "my-product",
  "roblox_user_id": 123456,
  "amount": 15000,
  "total": 15012,
  "expires_at": "2026-10-01T12:30:00Z",
  "paid_at": "2026-10-01T12:12:45Z"
}
StatusMeaning
pendingWaiting for payment
paidPaid, and the player is whitelisted
expiredNot paid before expires_at
failedCancelled or failed at the gateway

Here product is the slug, while creating an order returns the product's name. Errors: 400 bad_request when the id is not a UUID, 404 order_not_found.

Purchase flow: create the order, show the QR code, then poll this endpoint every few seconds until the status is no longer pending.
GET/api/v1/whitelistAPI key

Lists a product's whitelist, or checks one player. Scope whitelist:read.

ParameterRequiredRules
productyesProduct slug
roblox_user_idnoDigits only, up to 15. When set, returns that one player instead of the list
limitno1 to 500, default 100
offsetnoDefault 0. Newest entries first
curl "https://YOUR_DOMAIN/api/v1/whitelist?product=my-product&limit=50" \
  -H "X-API-Key: lm_your_key_here"
200 OK
{
  "product": "my-product",
  "total": 1,
  "limit": 50,
  "offset": 0,
  "entries": [ { "roblox_user_id": 123456, "added_at": "2026-10-02T09:00:00Z" } ]
}

# with roblox_user_id=123456
{ "product": "my-product", "roblox_user_id": 123456, "whitelisted": true, "added_at": "2026-10-02T09:00:00Z" }

Other responses: 400 bad_request, 404 product_not_found, 403 insufficient_scope, 429 rate_limited.

POST/api/v1/whitelistAPI key

Adds players to a product's whitelist. Safe to repeat: players already listed are skipped. Scope whitelist:write.

FieldRequiredRules
productyesProduct slug
roblox_user_idone ofA single player id
roblox_user_idsone ofArray of up to 100 player ids
curl -X POST "https://YOUR_DOMAIN/api/v1/whitelist" \
  -H "X-API-Key: lm_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"product":"my-product","roblox_user_ids":[123456,789012]}'
200 OK
{ "ok": true, "product": "my-product", "roblox_user_ids": [123456, 789012] }

Other responses: 400 bad_request (invalid id, empty list or more than 100), 404 product_not_found, 403 insufficient_scope, 429 rate_limited, 502 write_failed.

DELETE/api/v1/whitelistAPI key

Removes players from a product's whitelist. Takes effect on the next /api/check. Scope whitelist:write.

Send product and roblox_user_id as query parameters, or send the same fields as POST in a JSON body (up to 100 ids).

curl -X DELETE "https://YOUR_DOMAIN/api/v1/whitelist?product=my-product&roblox_user_id=123456" \
  -H "X-API-Key: lm_your_key_here"
200 OK
{ "ok": true, "product": "my-product", "removed": 1 }

removed is 0 when the player was not on the list. Other responses: 400 bad_request, 404 product_not_found, 403 insufficient_scope, 429 rate_limited, 502 write_failed.

POST/api/webhooks/tokoshoppNo key

Body: { "transaction_id": "…" }. The body is only a hint. The server asks Tokoshopp for the real status before granting anything, so a forged request cannot give anyone access. It always answers 200 { "ok": true }.

If your Tokoshopp account has a callback URL setting, point it at https://YOUR_DOMAIN/api/webhooks/tokoshopp. If not, polling the order endpoint works on its own.

Dashboard endpoints

These are called by the dashboard itself and use a login token, not an API key.

POST /api/keys

Body { "name": "My game", "preset": "check" } (name up to 40 characters). preset is check or full; or send "scopes": [...] with any of the scopes listed above. Leave both out and the key gets full access. Returns 201 { "id", "key", "prefix", "scopes" }. The full key appears only in this response. Limit of 10 active keys per account. Errors: name_required, invalid_scopes, too_many_keys, save_failed.

GET, POST, DELETE /api/settings

GET returns { "connected", "last4" }. POST { "tokoshopp_api_key": "…" } saves the key (8 to 200 characters, no spaces), encrypted with AES-256-GCM. DELETE removes it. The key is never returned.

ARTAN account & finance

Finance operations are dashboard-only. The user connects an ARTAN SHOP username and password in LicenseManager; the server logs in to Tokoshopp, keeps the session token server-side, and exposes balance, TopUp, TopUp status, Withdraw, and local finance history to the signed-in dashboard.

POST /api/artan/account

Body { "username": "…", "password": "…" }. LicenseManager validates the credentials against POST /login before encrypting and storing the password server-side.

POST /api/artan/topup

Automated Top Up. Body { "code": "XLD5", "nomor": "08123456789" }. Requires the connected ARTAN account. The order is forwarded to the provider automatically after submission. Services are loaded through GET /api/artan/services?group=emoney or kuota.

POST /api/artan/withdraw

Manual Withdraw. Body { "jenis": "ewallet", "bank": "DANA", "nomor": "08123456789", "nama": "Budi Santoso", "jumlah": 50000 }. Minimum Rp3.000. LicenseManager applies the documented ARTAN fee rules before submitting the withdrawal request.

GET /api/artan/history

Returns the signed-in account's locally recorded TopUp and Withdraw operations. It does not expose the ARTAN login token or password.

GET /api/config

Returns the public values the dashboard needs, including the configured baseUrl.

Troubleshooting

You seeTry this
401 unauthorized on a checkThe key is wrong, revoked or incomplete. It must be lm_ plus 40 characters
{ "owned": false } for a buyerThe player is not on the whitelist, or the slug in your script differs from the product's slug
Roblox HttpService errorTurn on Allow HTTP Requests, and use a Script, not a LocalScript
403 insufficient_scopeThe key lacks the scope in the response's required field. Create a new key with Full access, or send the right scopes
429 rate_limitedToo many calls. Wait Retry-After seconds, cache results, and check once per player join
payment_not_configuredSave your Tokoshopp key in the Payments tab
product_not_for_saleSet the product price to 1000 or more
server_not_configuredAdd the four environment variables and redeploy
internal_errorOpen the server logs; they contain the real error