Documentation
Everything you need to obfuscate Luau: how the obfuscator works, the dashboard around it, the HTTP API, and the in-script macros.
Overview
The MoonVeil API obfuscates, prettifies, and minifies Luau over HTTP. The base URL is https://moonveil.cc/api, and every endpoint is versioned under /v2.
A successful response returns the transformed script as text/plain. Errors return JSON shaped like { "error": "..." }.
Authentication
Send your API key as a bearer token on every request. The endpoint examples below reuse this client:
const KEY = process.env.MOONVEIL_KEY!;
async function moonveil(op: string, body: object) {
const res = await fetch(`https://moonveil.cc/api/v2/${op}`, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!res.ok) throw new Error((await res.json()).error);
return res.text();
}Browser sessions authenticate with the token cookie instead, which the dashboard handles automatically. API keys can be generated in the Dashboard in the 'API Keys' section.
Account
Returns the authenticated account, its plan and limits, and this month's usage.
| Field | Type | Description |
|---|---|---|
id | string | Unique account id. |
email | string | Account email. |
username | string | Discord display name. |
plan | object | Active plan: name, maxScriptChars, monthlyQuota, allowedOptions (vms, compileTypes, compress, vmDebug). |
usage | object | This month's usage: used, quota, obfuscationCount, resetsAt. |
const res = await fetch("https://moonveil.cc/api/account", {
headers: { Authorization: `Bearer ${KEY}` },
});
const account = await res.json();Example response
{
"id": "217039...",
"email": "[email protected]",
"username": "cpnk",
"avatar": null,
"opLevel": 0,
"hasBilling": true,
"plan": {
"name": "Basic",
"maxScriptChars": 5000000,
"monthlyQuota": 500,
"allowedOptions": {
"vms": ["cat", "fox"],
"compileTypes": ["cff", "vm", "safeEnv"],
"compress": true,
"vmDebug": true,
}
},
"usage": { "used": 8, "quota": 500, "obfuscationCount": 142, "resetsAt": "2026-07-18T03:21:00Z" }
}Obfuscate
Runs the full obfuscation pipeline over a script. The body takes the source and an optional options object; any option you omit falls back to its default.
| Field | Type | Default | Description |
|---|---|---|---|
script | string | — | The Luau source to obfuscate. Required. |
options | object | { } | Obfuscation options — see the table below. Every field is optional. |
filename | string | undefined | undefined | Optional filename hint for the returned attachment. Omit the field when unused. |
useCredit | "no" | "overflow" | "yes" | undefined | undefined | optional — how the request pays. "no" (default) runs inside your plan and its monthly quota; "overflow" spends a credit only when the run doesn't fit the plan (locked options, a script over your size limit, or a spent monthly quota) and otherwise just uses your quota; "yes" always spends a credit and ignores the plan entirely |
options.*| Field | Type | Default | Description |
|---|---|---|---|
compileType | "cff" | "vm" | "safeEnv" | "cff" | Top-level compile mode for the whole script. |
vmType | "cat" | "fox" | "cat" | VM implementation, used when compileType is vm/safeEnv or for any MV_VM blocks. |
vmDebug | boolean | false | Emit debug info (line info and debug names) with virtualized functions, so errors report mv:<line> locations and callstacks with real function names. Only available on plans that allow debug VM types. |
safeEnvLock | "luau" | "rbx" | "luau" | Used when the mode is safeEnv, locks to the specified environment. |
cff | object | none | CFF settings. |
cff.decompose | boolean | false | Decompose control flow into a flattened state machine. |
cff.mangleNext | boolean | false | Mangle successor / dispatch indices. |
cff.mangleStrings | boolean | false | Mangle string constants. |
cff.mangleGlobals | boolean | false | Mangle global accesses. |
cff.mangleCfPercent | number | 0 | Insert fake branches based on the provided chance. Higher = slower. |
compress | boolean | false | Compresses the emitted bundle behind a loadstring decompressor. Only available on paid plans. |
envWrapper | boolean | false | Resolves globals through a sandboxed env wrapper instead of getfenv. Enables runtimes where getfenv isn't available, like lute. When enabled, external scripts (loadstring'd or required) can't read the obfuscated script's globals. |
const obfuscated = await moonveil("obf", {
script: 'print"meow"',
options: { compileType: "safeEnv", vmType: "fox", vmDebug: true, safeEnvLock: "rbx", cff: { decompose: true, mangleStrings: true }, compress: true, envWrapper: true },
});Set useCredit to "yes" to spend a credit on the request instead of relying on your plan: every feature unlocks and the monthly quota is bypassed. Credits can be refilled under Billing in your dashboard.
With useCredit: "overflow" the request spends a credit only when the run requires a plan-locked option, a script over your size limit, or a spent monthly quota; otherwise, it just uses your quota. Scripts are capped at 5 MB (5,000,000 characters) on credit runs, or at your plan's limit when that is higher; with "no" your plan's limit applies as usual.
const obfuscated = await moonveil("obf", {
script: 'print"meow"',
options: { compileType: "vm", vmType: "fox", compress: true },
useCredit: "overflow",
});Returns the obfuscated Luau as text/plain (sent as a obfuscated.lua attachment). Requesting an option your plan does not allow returns 403, unless the request pays with a credit.
Example response
-- This script was generated using MoonVeil 2.0.15-beta
-- (obfuscated bundle, tens of KB, truncated here)
return({...Prettify
Reformats a script into clean, readable source. The body is just { "script": "..." }. Returns the formatted source as text/plain.
const pretty = await moonveil("prettify", { script });Example response
local function verify(pw)
return pw == "s3cret"
endMinify
Folds constants, strips dead code, and shrinks a script. It runs the optimizer, not just whitespace removal. Body { "script": "..." }, returns text/plain.
const minified = await moonveil("minify", { script });Example response
local function verify(pw)return pw=="s3cret"end
Errors
Errors return a JSON envelope with a single error message and a matching status code.
400 | Malformed JSON, an unknown or invalid field (useCredit is a string enum, not a boolean), or the script exceeds the size limit. |
401 | Missing or invalid API key / session. |
402 | No credits available — the request needed a credit ("yes", or "overflow" on a run that doesn't fit the plan) but the account has none. |
403 | A requested option is not allowed on your plan. |
429 | Monthly quota reached (the response includes recommendUseCredit and creditsAvailable when you have credits — retry with "useCredit": "overflow"), or you are being rate limited. |
500 | Something failed on our side. |
Rate limits
Each plan sets a monthly obfuscation quota, a script size limit, and a short-window rate limit. Obfuscation counts against both; prettify and minify are only rate limited. When you hit a limit you get 429, with a Retry-After header on the rate-limit case.
Requests that pay with a credit (useCredit: "yes", or "overflow" on a run that doesn't fit the plan) unlock every feature and bypass the monthly quota — each one costs a credit. Scripts are capped at 5 MB (5,000,000 characters) on credit runs, raised to your plan's limit when that is higher; with useCredit: "no" the plan's own size limit applies. Rate limits still apply to every request.