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

GET/api/account

Returns the authenticated account, its plan and limits, and this month's usage.

FieldTypeDescription
idstringUnique account id.
emailstringAccount email.
usernamestringDiscord display name.
planobjectActive plan: name, maxScriptChars, monthlyQuota, allowedOptions (vms, compileTypes, compress, vmDebug).
usageobjectThis 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
200 OKapplication/json
{
  "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

POST/api/v2/obf

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.

Request body
FieldTypeDefaultDescription
scriptstring—The Luau source to obfuscate. Required.
optionsobject{ }Obfuscation options — see the table below. Every field is optional.
filenamestring | undefinedundefinedOptional filename hint for the returned attachment. Omit the field when unused.
useCredit"no" | "overflow" | "yes" | undefinedundefinedoptional — 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.*
FieldTypeDefaultDescription
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.
vmDebugbooleanfalseEmit 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.
cffobjectnoneCFF settings.
cff.decomposebooleanfalseDecompose control flow into a flattened state machine.
cff.mangleNextbooleanfalseMangle successor / dispatch indices.
cff.mangleStringsbooleanfalseMangle string constants.
cff.mangleGlobalsbooleanfalseMangle global accesses.
cff.mangleCfPercentnumber0Insert fake branches based on the provided chance. Higher = slower.
compressbooleanfalseCompresses the emitted bundle behind a loadstring decompressor. Only available on paid plans.
envWrapperbooleanfalseResolves 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
200 OKtext/plain
-- This script was generated using MoonVeil 2.0.15-beta
-- (obfuscated bundle, tens of KB, truncated here)
return({...

Prettify

POST/api/v2/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
200 OKtext/plain
local function verify(pw)
    return pw == "s3cret"
end

Minify

POST/api/v2/minify

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
200 OKtext/plain
local function verify(pw)return pw=="s3cret"end

Errors

Errors return a JSON envelope with a single error message and a matching status code.

400Malformed JSON, an unknown or invalid field (useCredit is a string enum, not a boolean), or the script exceeds the size limit.
401Missing or invalid API key / session.
402No credits available — the request needed a credit ("yes", or "overflow" on a run that doesn't fit the plan) but the account has none.
403A requested option is not allowed on your plan.
429Monthly quota reached (the response includes recommendUseCredit and creditsAvailable when you have credits — retry with "useCredit": "overflow"), or you are being rate limited.
500Something 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.