Modly

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
}
  • 401 Missing or invalid API key (unauthorized, invalid_key)
  • 403 Missing scope (insufficient_scope)
  • 404 Server or module not found (not_found)
  • 429 Rate limit exceeded – per key, or per IP for requests without a valid key (rate_limited); see Retry-After
  • 500 Internal 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
}
  • 401 Missing or invalid API key (unauthorized, invalid_key)
  • 403 Missing scope (insufficient_scope)
  • 404 Server or module not found (not_found)
  • 429 Rate limit exceeded – per key, or per IP for requests without a valid key (rate_limited); see Retry-After
  • 500 Internal 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
}
  • 401 Missing or invalid API key (unauthorized, invalid_key)
  • 403 Missing scope (insufficient_scope)
  • 404 Unknown module (not_found)
  • 429 Rate limit exceeded – per key, or per IP for requests without a valid key (rate_limited); see Retry-After
  • 500 Internal 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
}
  • 401 Missing or invalid API key (unauthorized, invalid_key)
  • 403 Missing scope (insufficient_scope)
  • 404 Unknown module (not_found)
  • 429 Rate limit exceeded – per key, or per IP for requests without a valid key (rate_limited); see Retry-After
  • 500 Internal 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
}
  • 400 Invalid JSON, invalid body or config does not match the schema (invalid_json, validation_error, invalid_config)
  • 401 Missing or invalid API key (unauthorized, invalid_key)
  • 403 Missing scope (insufficient_scope) key issuer/reference authorization failed (forbidden), or enabling a Premium module on a free server (premium_required)
  • 404 Unknown module (not_found)
  • 409 expectedRevision is outdated (conflict)
  • 413 Body larger than 64 KiB (payload_too_large)
  • 429 Rate limit exceeded – per key, or per IP for requests without a valid key (rate_limited); see Retry-After
  • 500 Internal 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
}
  • 400 Invalid query (validation_error)
  • 401 Missing or invalid API key (unauthorized, invalid_key)
  • 403 Missing scope (insufficient_scope)
  • 404 Server or module not found (not_found)
  • 429 Rate limit exceeded – per key, or per IP for requests without a valid key (rate_limited); see Retry-After
  • 500 Internal 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
}
  • 400 Invalid query (validation_error)
  • 401 Missing or invalid API key (unauthorized, invalid_key)
  • 403 Missing scope (insufficient_scope)
  • 404 Server or module not found (not_found)
  • 429 Rate limit exceeded – per key, or per IP for requests without a valid key (rate_limited); see Retry-After
  • 500 Internal 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
}