Modly Developer API
Read and change your server's Modly settings from your own tools. Keys are created per server in the dashboard.
Developers · MCP · openapi.json · OpenAPI 3.1.0 · Base URL /api/v1
Authentication
Send the key as a bearer token. Each key belongs to one server and only has the permissions (scopes) chosen when it was created.
curl -H "Authorization: Bearer mdly_…" https://modly.cc/api/v1/guild
Every response uses the same envelope with success, data, error and meta. Errors carry a machine-readable code.
{
"success": true,
"data": {},
"error": null,
"meta": null
}Quickstart and examples
Paste a server API key when prompted. Create it in Dashboard → your server → Developer API with read:modules.
read -r -s MODLY_API_KEY export MODLY_API_KEY curl --fail-with-body https://modly.cc/api/v1/modules \ -H "Authorization: Bearer $MODLY_API_KEY"
429: REST and MCP share the per-key quota: 60 requests/minute on Free, 600 on Premium. Honor Retry-After before retrying.
A leaderboard in Node.js
const key = process.env.MODLY_API_KEY;
if (!key) throw new Error("Set MODLY_API_KEY");
const response = await fetch(
"https://modly.cc/api/v1/leveling/leaderboard?limit=10",
{ headers: { Authorization: `Bearer ${key}` } },
);
const result = await response.json();
if (!response.ok) throw new Error(result.error.message);
console.log(result.data);Update without losing changes
Requires read:config and write:config. This example enables leveling while preserving every setting. Fetch /modules/leveling/schema to inspect supported fields. A PUT replaces the full config; omitted fields use defaults. JSON Schema cannot represent every runtime refinement.
Configuration writes also check the key creator’s current Discord management permissions and role hierarchy. Referenced roles and channels must belong to the key’s server. If Discord cannot verify these checks, writes fail closed with forbidden. Grants also cannot add permissions the issuer does not have.
const base = "https://modly.cc/api/v1/modules/leveling/config";
const headers = {
Authorization: `Bearer ${process.env.MODLY_API_KEY}`,
"Content-Type": "application/json",
};
const currentResponse = await fetch(base, { headers });
const current = await currentResponse.json();
if (!currentResponse.ok) throw new Error(current.error.message);
const updated = await fetch(base, {
method: "PUT",
headers,
body: JSON.stringify({
config: current.data.config,
enabled: true,
expectedRevision: current.data.revision,
}),
});
const result = await updated.json();
if (updated.status === 409) {
throw new Error("Config changed. Read it again and review the change.");
}
if (!updated.ok) throw new Error(result.error.message);
console.log(result.data.revision);Endpoints
GET/guild
Server of the key · any key
Response
JSON Schema
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"const": true
},
"data": {
"$ref": "#/components/schemas/Guild"
},
"error": {
"type": "null"
},
"meta": {
"type": "null"
}
},
"required": [
"success",
"data",
"error",
"meta"
],
"additionalProperties": false
}401Missing or invalid API key (unauthorized, invalid_key)403Missing scope (insufficient_scope)404Server or module not found (not_found)429Rate limit exceeded – per key, or per IP for requests without a valid key (rate_limited); see Retry-After500Internal error (internal)
GET/modules
Modules with status · scope read:modules
Requires the `read:modules` scope.
Response
JSON Schema
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"const": true
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ModuleSummary"
}
},
"error": {
"type": "null"
},
"meta": {
"type": "null"
}
},
"required": [
"success",
"data",
"error",
"meta"
],
"additionalProperties": false
}401Missing or invalid API key (unauthorized, invalid_key)403Missing scope (insufficient_scope)404Server or module not found (not_found)429Rate limit exceeded – per key, or per IP for requests without a valid key (rate_limited); see Retry-After500Internal error (internal)
GET/modules/{moduleId}/schema
Get module configuration JSON Schema · scope read:config
Requires the `read:config` scope.
moduleId(path, required){"type":"string","pattern":"^[a-z][a-z0-9-]{1,30}$","example":"leveling"}
Response
JSON Schema
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"const": true
},
"data": {
"$ref": "#/components/schemas/ModuleSchema"
},
"error": {
"type": "null"
},
"meta": {
"type": "null"
}
},
"required": [
"success",
"data",
"error",
"meta"
],
"additionalProperties": false
}401Missing or invalid API key (unauthorized, invalid_key)403Missing scope (insufficient_scope)404Unknown module (not_found)429Rate limit exceeded – per key, or per IP for requests without a valid key (rate_limited); see Retry-After500Internal error (internal)
GET/modules/{moduleId}/config
Get a module config · scope read:config
Requires the `read:config` scope.
moduleId(path, required){"type":"string","pattern":"^[a-z][a-z0-9-]{1,30}$","example":"leveling"}
Response
JSON Schema
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"const": true
},
"data": {
"$ref": "#/components/schemas/ModuleConfig"
},
"error": {
"type": "null"
},
"meta": {
"type": "null"
}
},
"required": [
"success",
"data",
"error",
"meta"
],
"additionalProperties": false
}401Missing or invalid API key (unauthorized, invalid_key)403Missing scope (insufficient_scope)404Unknown module (not_found)429Rate limit exceeded – per key, or per IP for requests without a valid key (rate_limited); see Retry-After500Internal error (internal)
PUT/modules/{moduleId}/config
Replace a module config · scope write:config
Requires the `write:config` scope. Validates against the module's config schema, stores a new revision, writes the audit log (source `api`) and reloads the config in the bot immediately.
moduleId(path, required){"type":"string","pattern":"^[a-z][a-z0-9-]{1,30}$","example":"leveling"}
Request body
{
"$ref": "#/components/schemas/ConfigUpdate"
}Response
JSON Schema
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"const": true
},
"data": {
"$ref": "#/components/schemas/ModuleConfig"
},
"error": {
"type": "null"
},
"meta": {
"type": "null"
}
},
"required": [
"success",
"data",
"error",
"meta"
],
"additionalProperties": false
}400Invalid JSON, invalid body or config does not match the schema (invalid_json, validation_error, invalid_config)401Missing or invalid API key (unauthorized, invalid_key)403Missing scope (insufficient_scope) key issuer/reference authorization failed (forbidden), or enabling a Premium module on a free server (premium_required)404Unknown module (not_found)409expectedRevision is outdated (conflict)413Body larger than 64 KiB (payload_too_large)429Rate limit exceeded – per key, or per IP for requests without a valid key (rate_limited); see Retry-After500Internal error (internal)
GET/leveling/leaderboard
Leveling leaderboard · scope read:leveling
Requires the `read:leveling` scope.
page(query){"default":1,"type":"integer","minimum":1,"maximum":10000}limit(query){"default":25,"type":"integer","minimum":1,"maximum":100}
Response
JSON Schema
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"const": true
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LeaderboardEntry"
}
},
"error": {
"type": "null"
},
"meta": {
"$ref": "#/components/schemas/PageMeta"
}
},
"required": [
"success",
"data",
"error",
"meta"
],
"additionalProperties": false
}400Invalid query (validation_error)401Missing or invalid API key (unauthorized, invalid_key)403Missing scope (insufficient_scope)404Server or module not found (not_found)429Rate limit exceeded – per key, or per IP for requests without a valid key (rate_limited); see Retry-After500Internal error (internal)
GET/moderation/cases
Moderation cases, newest first · scope read:moderation
Requires the `read:moderation` scope.
page(query){"default":1,"type":"integer","minimum":1,"maximum":10000}limit(query){"default":25,"type":"integer","minimum":1,"maximum":100}targetId(query){"type":"string","pattern":"^\\d{17,20}$"}action(query){"type":"string","enum":["warn","timeout","untimeout","kick","ban","unban"]}
Response
JSON Schema
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"const": true
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ModerationCase"
}
},
"error": {
"type": "null"
},
"meta": {
"$ref": "#/components/schemas/PageMeta"
}
},
"required": [
"success",
"data",
"error",
"meta"
],
"additionalProperties": false
}400Invalid query (validation_error)401Missing or invalid API key (unauthorized, invalid_key)403Missing scope (insufficient_scope)404Server or module not found (not_found)429Rate limit exceeded – per key, or per IP for requests without a valid key (rate_limited); see Retry-After500Internal error (internal)
Webhooks
Outgoing webhooks are signed like Standard Webhooks, so you can verify them with the official libraries or a few lines of code.
{
"type": "leveling.level-up",
"timestamp": "2026-10-13T10:00:00.000Z",
"guildId": "…",
"data": {}
}import { createHmac, timingSafeEqual } from "node:crypto";
function verifyWebhook(rawBody, headers, secret) {
const id = headers.get("webhook-id");
const timestamp = headers.get("webhook-timestamp");
const signatures = headers.get("webhook-signature") ?? "";
if (!id || !timestamp || !/^\d+$/.test(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key)
.update(`${id}.${timestamp}.${rawBody}`).digest();
return signatures.split(/\s+/).some((signature) => {
const [version, encoded] = signature.split(",");
if (version !== "v1" || !encoded) return false;
const actual = Buffer.from(encoded, "base64");
return actual.length === expected.length && timingSafeEqual(actual, expected);
});
}Verify the untouched raw body before JSON parsing; reject on false. Use the webhook-id as an idempotency key because retries can deliver the same event more than once. Keep the signing secret on your server.
Schemas
ConfigUpdate
JSON Schema
{
"type": "object",
"properties": {
"config": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {},
"description": "Complete config; validated against the module's config schema"
},
"enabled": {
"type": "boolean"
},
"expectedRevision": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "Optimistic locking: the revision you read (0 for a never-saved config)"
}
},
"required": [
"config",
"expectedRevision"
],
"additionalProperties": false
}Guild
JSON Schema
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"icon": {
"type": [
"string",
"null"
]
},
"locale": {
"type": "string"
},
"tier": {
"type": "string",
"enum": [
"free",
"premium"
]
}
},
"required": [
"id",
"name",
"icon",
"locale",
"tier"
],
"additionalProperties": false
}ErrorEnvelope
JSON Schema
{
"type": "object",
"properties": {
"success": {
"type": "boolean",
"const": false
},
"data": {
"type": "null"
},
"error": {
"$ref": "#/components/schemas/ApiError"
},
"meta": {
"type": "null"
}
},
"required": [
"success",
"data",
"error",
"meta"
],
"additionalProperties": false,
"description": "Uniform error response"
}ApiError
JSON Schema
{
"type": "object",
"properties": {
"code": {
"$ref": "#/components/schemas/ApiErrorCode"
},
"message": {
"type": "string"
},
"details": {
"description": "e.g. validation issues for validation_error/invalid_config"
}
},
"required": [
"code",
"message"
],
"additionalProperties": false
}ApiErrorCode
JSON Schema
{
"type": "string",
"enum": [
"unauthorized",
"forbidden_origin",
"forbidden",
"invalid_key",
"insufficient_scope",
"rate_limited",
"not_found",
"invalid_json",
"validation_error",
"invalid_config",
"payload_too_large",
"premium_required",
"conflict",
"internal"
]
}ModuleSummary
JSON Schema
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"tier": {
"type": "string",
"enum": [
"free",
"premium"
],
"description": "Minimum tier to enable the module"
},
"enabled": {
"type": "boolean",
"description": "Switched on (stored or default)"
},
"availability": {
"type": "string",
"enum": [
"enabled",
"disabled",
"premium_required"
]
}
},
"required": [
"id",
"name",
"tier",
"enabled",
"availability"
],
"additionalProperties": false
}ModuleSchema
JSON Schema
{
"type": "object",
"properties": {
"moduleId": {
"type": "string"
},
"schemaVersion": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"schema": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {}
}
},
"required": [
"moduleId",
"schemaVersion",
"schema"
],
"additionalProperties": false,
"description": "JSON Schema for config input; runtime validation additionally enforces refinements"
}ModuleConfig
JSON Schema
{
"type": "object",
"properties": {
"moduleId": {
"type": "string"
},
"enabled": {
"type": "boolean"
},
"revision": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "0 = never saved; pass as expectedRevision"
},
"schemaVersion": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"config": {
"type": "object",
"propertyNames": {
"type": "string"
},
"additionalProperties": {},
"description": "Validated; missing fields filled with defaults"
},
"valid": {
"type": "boolean",
"description": "false = stored config is invalid, defaults are active"
}
},
"required": [
"moduleId",
"enabled",
"revision",
"schemaVersion",
"config",
"valid"
],
"additionalProperties": false
}LeaderboardEntry
JSON Schema
{
"type": "object",
"properties": {
"rank": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"userId": {
"type": "string"
},
"xp": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"level": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"messages": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"voiceSeconds": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
}
},
"required": [
"rank",
"userId",
"xp",
"level",
"messages",
"voiceSeconds"
],
"additionalProperties": false
}PageMeta
JSON Schema
{
"type": "object",
"properties": {
"page": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"total": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}
},
"required": [
"page",
"limit",
"total"
],
"additionalProperties": false
}ModerationCase
JSON Schema
{
"type": "object",
"properties": {
"caseNumber": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"action": {
"type": "string",
"enum": [
"warn",
"timeout",
"untimeout",
"kick",
"ban",
"unban"
]
},
"targetId": {
"type": "string"
},
"moderatorId": {
"type": "string"
},
"reason": {
"type": [
"string",
"null"
]
},
"durationSeconds": {
"anyOf": [
{
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
{
"type": "null"
}
]
},
"expiresAt": {
"anyOf": [
{
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
{
"type": "null"
}
]
},
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
"revokedAt": {
"anyOf": [
{
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"
},
{
"type": "null"
}
]
}
},
"required": [
"caseNumber",
"action",
"targetId",
"moderatorId",
"reason",
"durationSeconds",
"expiresAt",
"createdAt",
"revokedAt"
],
"additionalProperties": false
}