API Echo

Public REST API and webhooks: connect your server’s economy, levels and monitoring to your own services — a game server, website, bot or automations.

https://api.echo-assistants.tech

Authorization

Every request needs a key in the Authorization header. A key is issued to a a server, not an account. Only the server owner can create one in the dashboard → “Developers”: up to 3 (10 with Premium) keys. The /catalog/* endpoints are public — no key needed (60/min per-IP limit, responses are cached).

curl https://api.echo-assistants.tech/me \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Create a key

Key security

A key starts with echoapi_, followed by 30 random Latin letters and digits and a 6-character checksum. A string with a wrong checksum is rejected by the API right away (401). Keys of the old format (echo_…) keep working; a reissued key gets the new format.

Pattern for finding keys in your own code
\bechoapi_[0-9A-Za-z]{36}\b

A key exposed in public is disabled automatically. Through the Secret Scanning program GitHub reports keys found in public repositories, and the Echo bot spots keys in messages on servers and deletes those messages. The server owner and the key creator are notified on the site, in Telegram and by email, with a link to where the key was found.

What to do: open the server's keys in the developer portal and click Reissue on the key card. The key gets a new secret and works again; the old one stops working immediately. Replace the key wherever it is stored. A found key can't be turned back on without reissuing. If you no longer need the key, revoke it.

Keep the key on your own server — in environment variables or a secret store. Don't put it into page or app code, repositories, messages or screenshots.

Service

GET/meno scope

Key check: returns the server and its scopes.

Example
curl "https://api.echo-assistants.tech/me" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Response
{ "guildId": "1369...", "scopes": ["levels:read"] }

Economy

GET/economy/balance?user=<id>economy:read

Member balance: wallet, bank, chips.

Example
curl "https://api.echo-assistants.tech/economy/balance?user=123456789012345678" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Response
{ "user": "123...", "balance": 1500, "bank": 200, "chips": 0 }
POST/economy/balanceeconomy:write

Add or deduct currency. delta may be negative; the balance stays within 0…2,000,000,000.

Example
curl -X POST "https://api.echo-assistants.tech/economy/balance" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{ "user": "123...", "delta": 250 }'
Request body
{ "user": "123...", "delta": 250 }
Response
{ "ok": true, "user": "123...", "balance": 1750 }
GET/economy/leaderboard?limit=<n>economy:read

Top members by balance (limit 1–100, default 20).

Example
curl "https://api.echo-assistants.tech/economy/leaderboard?limit=20" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Response
{ "guildId": "1369...", "leaderboard": [ { "rank": 1, "userId": "627...", "balance": 15000, "bank": 3000 } ] }

Levels

GET/levels/leaderboard?limit=<n>levels:read

Top members by experience (limit 1–100, default 20).

Example
curl "https://api.echo-assistants.tech/levels/leaderboard?limit=20" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Response
{ "guildId": "1369...", "leaderboard": [ { "rank": 1, "userId": "627...", "xp": 3965, "messages": 374, "voiceSeconds": 1273 } ] }
GET/levels/xp?user=<id>levels:read

A member's XP and level; nextLevelXp is the total XP needed for the next level.

Example
curl "https://api.echo-assistants.tech/levels/xp?user=123456789012345678" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Response
{ "user": "123...", "xp": 1320, "level": 4, "nextLevelXp": 1500 }
POST/levels/xplevels:write

Add or remove XP: delta is a whole number, up to ±1,000,000 per request; XP never goes below zero. The level is recalculated at once. If it went up, the bot grants level rewards (roles, coins, announcement) just like regular levelling and sends the member.level_up event marked "via": "api". Lowering the level does not take rewards back. Server members only (otherwise 404 not_member); if levels are turned off on the server — 409 levels_off.

Example
curl -X POST "https://api.echo-assistants.tech/levels/xp" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{ "user": "123...", "delta": 250 }'
Request body
{ "user": "123...", "delta": 250 }
Response
{ "ok": true, "user": "123...", "xp": 1570, "level": 5, "previousXp": 1320, "previousLevel": 4, "levelUp": true }

Monitoring

GET/monitoring/servermonitoring:read

Server card in the catalog: Flames for the period, total, top rank and statuses.

Example
curl "https://api.echo-assistants.tech/monitoring/server" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Response
{ "guildId": "1369...", "slug": "my-server", "flames": 420, "flamesTotal": 9100, "published": true, "rank": 3, "partner": true, "verified": true }
GET/monitoring/top?limit=<n>monitoring:read

Top catalog servers by Flames (limit 1–100, default 20).

Example
curl "https://api.echo-assistants.tech/monitoring/top?limit=20" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Response
{ "top": [ { "rank": 1, "guildId": "1369...", "slug": "my-server", "flames": 980, "flamesTotal": 21000 } ] }

Integrations

Connect your game, website or CRM: member attributes become roles by the rules in the dashboard → “Integrations”, players link accounts with a code, and allowed roles can be granted directly. Write bodies are JSON up to 8 KB.

PUT/members/<discordId>/attributesmembers:write

Send a member's attributes (full replace): up to 25 fields, names in Latin letters/digits/_, values a string up to 100 chars, a number or true/false. The bot recalculates rule roles right away.

Example
curl -X PUT "https://api.echo-assistants.tech/members/123456789012345678/attributes" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{ "attributes": { "faction": "police", "rank": 3, "vip": true } }'
Request body
{ "attributes": { "faction": "police", "rank": 3, "vip": true } }
Response
{ "ok": true, "discordId": "123...", "attributes": { "faction": "police", "rank": 3, "vip": true } }
GET/members/<discordId>/attributesmembers:read

What was sent for the member and which external account they linked.

timedRoles — roles allowed for the API that were granted for a limited time, and when the bot will remove them.

Example
curl "https://api.echo-assistants.tech/members/123456789012345678/attributes" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Response
{ "discordId": "123...", "externalId": "Ivan_Petrov", "attributes": { "faction": "police", "rank": 3 }, "timedRoles": [ { "roleId": "1369...", "expiresAt": "2026-10-10T12:00:00.000Z" } ] }
DELETE/members/<discordId>/attributesmembers:write

Clear all of a member's attributes. The bot removes roles granted by rules and leaves manually given roles alone.

Example
curl -X DELETE "https://api.echo-assistants.tech/members/123456789012345678/attributes" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Response
{ "ok": true, "removed": true }
PUT/external/<externalId>/attributesmembers:write

The same by the ID in your system (nickname, UUID, license:…). Applies to whoever linked the account with a code; until then it is stored and applied on linking. Attributes sent by Discord ID win on name clashes.

Example
curl -X PUT "https://api.echo-assistants.tech/external/Ivan_Petrov/attributes" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{ "attributes": { "faction": "army", "rank": 5 } }'
Request body
{ "attributes": { "faction": "army", "rank": 5 } }
Response
{ "ok": true, "externalId": "Ivan_Petrov", "discordId": "123...", "attributes": { "faction": "army", "rank": 5 } }
GET/external/<externalId>/attributesmembers:read

What was sent for the external account and who linked it (discordId: null — nobody yet).

Example
curl "https://api.echo-assistants.tech/external/Ivan_Petrov/attributes" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Response
{ "externalId": "Ivan_Petrov", "discordId": "123...", "attributes": { "faction": "army", "rank": 5 } }
DELETE/external/<externalId>/attributesmembers:write

Clear all attributes of an external account.

Example
curl -X DELETE "https://api.echo-assistants.tech/external/Ivan_Petrov/attributes" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Response
{ "ok": true, "removed": true }
POST/link-codeslinks:write

A link code for a player: 8 characters, single-use, valid for ttlMinutes (1–60, default 15). The player presses “Link account” on the server panel (or uses /link) and enters it. A new code for the same account voids the previous one.

Example
curl -X POST "https://api.echo-assistants.tech/link-codes" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{ "externalId": "Ivan_Petrov", "ttlMinutes": 15 }'
Request body
{ "externalId": "Ivan_Petrov", "ttlMinutes": 15 }
Response
{ "code": "K7PQ3MZA", "display": "K7PQ-3MZA", "externalId": "Ivan_Petrov", "expiresAt": "2026-09-29T12:15:00.000Z" }
GET/links?externalId=<externalId>links:read

Whose account: by externalId or by discordId (?discordId=…). No link — 404 not_linked.

Example
curl "https://api.echo-assistants.tech/links?externalId=Ivan_Petrov" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Response
{ "externalId": "Ivan_Petrov", "discordId": "123...", "linkedAt": "2026-09-29T12:03:11.000Z" }
DELETE/links?externalId=<externalId>links:write

Unlink an account (or ?discordId=…). The external account's attributes stop applying to the person.

Example
curl -X DELETE "https://api.echo-assistants.tech/links?externalId=Ivan_Petrov" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Response
{ "ok": true, "externalId": "Ivan_Petrov", "discordId": "123..." }
POST/members/<discordId>/rolesroles:write

Grant and remove roles directly — only those the owner marked in the developer portal → “API keys” → “Allowed for keys” (up to 10 per list). remove takes off any allowed role, even one given manually. The bot does it: the reply is 202 with a job number.

durationMinutes (1–525,600) — grant the roles from add for a limited time: the bot removes them once it is over. The term never shortens one already set, and a role the member already had without a term stays permanent. add without durationMinutes makes the role permanent, remove cancels a scheduled removal.

Example
curl -X POST "https://api.echo-assistants.tech/members/123456789012345678/roles" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{ "add": ["1369..."], "remove": ["1370..."], "durationMinutes": 1440 }'
Request body
{ "add": ["1369..."], "remove": ["1370..."], "durationMinutes": 1440 }
Response
{ "ok": true, "opId": 42, "status": "pending" }
GET/role-ops/<opId>roles:write

The result of a grant job: pending, done or failed, and what happened with each role.

For a timed grant, result.expiresAt shows when the bot will remove each granted role (null — the member already had the role without a term, and it stays permanent).

Example
curl "https://api.echo-assistants.tech/role-ops/42" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Response
{ "opId": 42, "discordId": "123...", "status": "done", "durationMinutes": 1440, "result": { "added": ["1369..."], "removed": [], "failed": [], "expiresAt": { "1369...": "2026-10-10T12:00:00.000Z" } } }
POST/channels/<channelId>/messagesmessages:write

A message as the bot (the server's custom bot if connected) — only to channels the owner ticked in the developer portal → “API keys” → “Allowed for keys” (up to 10). content — up to 2000 characters, embeds — up to 10 simple embeds: title, description, url, color, timestamp, author.name, footer.text, image.url, thumbnail.url, fields; links must be https. Mentions ping nobody by default: only server members listed in mentionUsers (up to 10) can be pinged, and @everyone, @here and roles never are. Request body — up to 32 KB. The bot does it: the reply is 202 with a job number.

Example
curl -X POST "https://api.echo-assistants.tech/channels/137100000000000000/messages" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{ "content": "Сервер перезапустится через 5 минут", "embeds": [ { "title": "Технические работы", "description": "Начало в 03:00 МСК", "color": 9133302 } ], "mentionUsers": ["123..."] }'
Request body
{ "content": "Сервер перезапустится через 5 минут", "embeds": [ { "title": "Технические работы", "description": "Начало в 03:00 МСК", "color": 9133302 } ], "mentionUsers": ["123..."] }
Response
{ "ok": true, "opId": 77, "status": "pending" }
GET/message-ops/<opId>messages:write

The result of sending: pending, done (message id and link) or failed with a reason: channel_not_allowed, channel_missing, missing_permissions, rejected.

Example
curl "https://api.echo-assistants.tech/message-ops/42" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Response
{ "opId": 77, "channelId": "1371...", "status": "done", "result": { "messageId": "1372...", "channelId": "1371...", "url": "https://discord.com/channels/1369.../1371.../1372..." } }

Catalog

GET/catalog/servers?search=&tag=&lang=&limit=public · no key

Search and list servers in the monitoring catalog. Fully public — no key needed.

Example
curl "https://api.echo-assistants.tech/catalog/servers"
Response
{ "count": 1, "servers": [ { "slug": "my-server", "name": "My Server", "memberCount": 5200, "flames": 980, "tags": ["Игры"], "language": "ru", "invite": "https://discord.gg/...", "badges": { "verified": true } } ] }
GET/catalog/top?limit=<n>public · no key

Catalog top by Flames.

Example
curl "https://api.echo-assistants.tech/catalog/top?limit=20"
Response
{ "top": [ { "rank": 1, "slug": "my-server", "name": "My Server", "flames": 980 } ] }
GET/catalog/server/<slug>public · no key

Public server card: description, Flames, rank, tags, badges, invite, rating.

Example
curl "https://api.echo-assistants.tech/catalog/server/my-server"
Response
{ "slug": "my-server", "name": "My Server", "flames": 980, "rank": 3, "rating": { "avg": 8.7, "count": 42 }, "invite": "https://discord.gg/..." }
GET/catalog/reviews/<slug>?limit=<n>public · no key

Server reviews with an average rating.

Example
curl "https://api.echo-assistants.tech/catalog/reviews/my-server?limit=20"
Response
{ "slug": "my-server", "summary": { "avg": 8.7, "count": 42 }, "reviews": [ { "user": "Ник", "rating": 9, "comment": "Топ", "at": "..." } ] }
GET/catalog/tagspublic · no key

Catalog tags/categories with server counts.

Example
curl "https://api.echo-assistants.tech/catalog/tags"
Response
{ "tags": [ { "tag": "Игры", "count": 128 } ] }

Webhooks

Echo pushes events to your URL in real time. Set up a webhook in the dashboard → “Developers”. Each request is signed with the X-Echo-Signature header (HMAC-SHA256 of the body using your secret); the event name is in X-Echo-Event.

flame.bump

The server received Flame, in the catalog or via /flame.

{
  "event": "flame.bump",
  "guildId": "1369...",
  "flames": 421,
  "flamesTotal": 9101,
  "amount": 1,
  "source": "web",
  "at": "2026-07-27T20:30:00.000Z"
}
test

Sent by the “Send test” button — to check that the receiver works.

{ "event": "test", "guildId": "1369...", "message": "Проверка вебхука Echo", "at": "..." }
Signature verification
import crypto from "node:crypto";

// body is the raw request text, secret comes from the dashboard
const expected = "sha256=" +
  crypto.createHmac("sha256", secret).update(body).digest("hex");
const header = req.headers["x-echo-signature"];
const got = Buffer.from(typeof header === "string" ? header : "");
const want = Buffer.from(expected);
const ok = got.length === want.length && crypto.timingSafeEqual(got, want);

Integration notifications (Premium)

Configured in the dashboard → “Integrations”. Echo sends a JSON POST {"id", "event", "guildId", "at", "data"}: the signature is X-Echo-Signature (sha256= plus HMAC-SHA256 of the raw body with your secret), the event name is in X-Echo-Event, the delivery number in X-Echo-Delivery.

Reply with 2xx within 8 seconds. Otherwise we retry after 30 s, 2 min, 10 min and 1 h with the same id; redirects are not followed. The URL must be https on 443 with a public IP. After 20 failures in a row sending is turned off — re-enable it in the dashboard once your receiver works.

member.join

A member joined the server.

{ "id": "evt_…", "event": "member.join", "guildId": "1369...", "at": "2026-09-29T12:00:00.000Z",
  "data": { "discordId": "123...", "username": "ivan", "joinedAt": "2026-09-29T12:00:00.000Z" } }
member.leave

A member left or was removed.

{ "id": "evt_…", "event": "member.leave", "guildId": "1369...", "at": "…", "data": { "discordId": "123...", "username": "ivan" } }
member.level_up

A member levelled up.

{ "id": "evt_…", "event": "member.level_up", "guildId": "1369...", "at": "…", "data": { "discordId": "123...", "oldLevel": 4, "newLevel": 5, "xp": 1320 } }
store.purchase

A purchase in the server store: a role granted, a coin pack credited (kind "coins", field coins) or a server donation. Anonymous donations have no buyer.

{ "id": "evt_…", "event": "store.purchase", "guildId": "1369...", "at": "…",
  "data": { "orderId": 815, "kind": "role", "itemName": "VIP", "roleId": "1369...", "buyerId": "123...", "discordId": "123...", "priceKopecks": 14900, "durationDays": 30 } }
ticket.opened

A ticket was opened.

{ "id": "evt_…", "event": "ticket.opened", "guildId": "1369...", "at": "…", "data": { "ticketId": 311, "number": 57, "discordId": "123...", "channelId": "1371...", "panelId": 4 } }
account.linked

A player linked an account with a code.

{ "id": "evt_…", "event": "account.linked", "guildId": "1369...", "at": "…",
  "data": { "externalId": "Ivan_Petrov", "discordId": "123...", "username": "ivan", "previousDiscordId": null, "previousExternalId": null } }
test

The “Send test” button in the dashboard → “Integrations”.

{ "id": "evt_…", "event": "test", "guildId": "1369...", "at": "…", "data": { "message": "…" } }
Signature check — Node.js
import crypto from "node:crypto";
import express from "express";
import pg from "pg";

const app = express();
const SECRET = process.env.ECHO_WEBHOOK_SECRET; // whsec_… из панели
const GUILD_ID = process.env.ECHO_GUILD_ID;
if (!SECRET || !GUILD_ID) throw new Error("ECHO_WEBHOOK_SECRET and ECHO_GUILD_ID required");
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

// Выполните миграцию в своей PostgreSQL заранее:
// CREATE TABLE echo_webhook_events (
//   guild_id text NOT NULL, event_id text NOT NULL,
//   PRIMARY KEY (guild_id, event_id)
// );
// Храните ID минимум 2 часа. Несколько процессов используют одну таблицу.

// the signature is computed over the raw body, before parsing JSON
app.post("/echo-webhook", express.raw({ type: "application/json", limit: "64kb" }), async (req, res) => {
  if (!Buffer.isBuffer(req.body)) return res.status(400).end();
  const expected = "sha256=" +
    crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
  const got = Buffer.from(req.get("X-Echo-Signature") ?? "");
  const want = Buffer.from(expected);
  const ok = got.length === want.length && crypto.timingSafeEqual(got, want);
  if (!ok) return res.status(401).end();

  let event;
  try { event = JSON.parse(req.body.toString("utf8")); }
  catch { return res.status(400).end(); }
  if (!event || typeof event !== "object" || Array.isArray(event) ||
      typeof event.id !== "string" || !/^evt_[0-9a-f-]{36}$/.test(event.id) ||
      event.guildId !== GUILD_ID || typeof event.event !== "string" ||
      typeof event.at !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(event.at)) return res.status(400).end();
  const at = Date.parse(event.at);
  const age = Date.now() - at;
  // Повторы сохраняют at; неверная дата и будущее дальше 5 минут — отказ.
  if (!Number.isFinite(at) || new Date(at).toISOString() !== event.at || age > 2 * 60 * 60_000 || age < -5 * 60_000) return res.status(400).end();

  let db;
  try {
    db = await pool.connect();
    await db.query("BEGIN");
    const claimed = await db.query(
      "INSERT INTO echo_webhook_events (guild_id, event_id) VALUES ($1, $2) ON CONFLICT DO NOTHING RETURNING event_id",
      [event.guildId, event.id],
    );
    if (!claimed.rowCount) {
      await db.query("ROLLBACK");
      return res.status(204).end();
    }
    // Изменение награды/баланса делайте через db В ЭТОЙ транзакции.
    // Для внешнего действия записывайте outbox здесь и обрабатывайте отдельно.
    await db.query("COMMIT");
    return res.status(204).end();
  } catch {
    if (db) await db.query("ROLLBACK").catch(() => {});
    return res.status(500).end();
  } finally {
    db?.release();
  }
});
Signature check — PHP
<?php
$secret = getenv('ECHO_WEBHOOK_SECRET'); // whsec_… из панели
$guildId = getenv('ECHO_GUILD_ID');
if (!$secret || !$guildId) { http_response_code(500); exit; }
// the signature is computed over the raw body, before parsing JSON
$body = file_get_contents('php://input');
if ($body === false || strlen($body) > 65536) { http_response_code(413); exit; }
$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);
$got = $_SERVER['HTTP_X_ECHO_SIGNATURE'] ?? '';
if (!hash_equals($expected, $got)) {
    http_response_code(401);
    exit;
}
$event = json_decode($body, true);
if (!is_array($event) || !is_string($event['id'] ?? null) ||
    !preg_match('/^evt_[0-9a-f-]{36}$/D', $event['id']) ||
    ($event['guildId'] ?? null) !== $guildId ||
    !is_string($event['event'] ?? null) || !is_string($event['at'] ?? null) ||
    !preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/D', $event['at'])) {
    http_response_code(400);
    exit;
}
$date = DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.v\Z', $event['at'], new DateTimeZone('UTC'));
$at = $date && $date->format('Y-m-d\TH:i:s.v\Z') === $event['at'] ? $date->getTimestamp() : false;
$age = $at === false ? null : time() - $at;
if ($age === null || $age > 7200 || $age < -300) {
    http_response_code(400); exit;
}
// Та же миграция PostgreSQL, что в Node.js-примере.
// ECHO_DATABASE_DSN: pgsql:host=…;dbname=… (PDO_PGSQL).
$db = new PDO(getenv('ECHO_DATABASE_DSN'), getenv('ECHO_DATABASE_USER'), getenv('ECHO_DATABASE_PASSWORD'),
    [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
try {
    $db->beginTransaction();
    $claim = $db->prepare('INSERT INTO echo_webhook_events (guild_id, event_id) VALUES (?, ?) ON CONFLICT DO NOTHING RETURNING event_id');
    $claim->execute([$event['guildId'], $event['id']]);
    if ($claim->fetchColumn() === false) {
        $db->rollBack(); http_response_code(204); exit;
    }
    // Изменение награды/баланса делайте через $db В ЭТОЙ транзакции.
    // Внешние действия — через outbox, записанный здесь.
    $db->commit();
    http_response_code(204);
} catch (Throwable $e) {
    if ($db->inTransaction()) $db->rollBack();
    http_response_code(500);
}

Scopes

When creating a key you choose permissions — each endpoint requires its own scope.

guild:read
Server data
Basic information about the server.
economy:read
Read economy
Balances: wallet, bank, chips.
economy:write
Write economy
Adding and deducting currency.
levels:read
Read levels
Experience and member leaderboard.
monitoring:read
Read monitoring
Flames, top rank, catalog statuses.
members:read
Read attributes
Read member attributes by Discord ID and by external system ID — without the right to change them.
members:write
Member attributes
Send attributes by Discord ID and by external system ID.
roles:write
Role grants
Grant and remove roles allowed in the developer portal → “API keys” → “Allowed for keys”.
links:read
Read links
Whose account: the external ID ↔ Discord link.
links:write
Link codes
Issue link codes and unlink accounts.
levels:write
XP write
Add and remove XP; level rewards are granted by the bot.
messages:write
Bot messages
Messages as the bot in channels allowed in the developer portal → “API keys” → “Allowed for keys”.

Limits and errors

Up to 3 (10 with Premium) keys per server, with a limit of 40 (120 with Premium) requests per minute per key. Errors are returned as JSON { "error": "..." } with the corresponding HTTP code.

CodeerrorWhen
400bad_fieldsInvalid or missing request parameters.
401unauthorizedKey not provided, invalid or revoked.
403insufficient_scopeThe key lacks the required scope for this request.
403key_paused_premiumThe key is paused: the server has no Premium, and without it only the keys within the free limit that were issued first work.
404not_linkedThe account isn't linked (link methods) or the job wasn't found.
409levels_offThe action does not fit the server settings: for example, levels are turned off.
413body_too_largeThe request body is larger than 8 KB.
429rate_limitedExceeded the limit of 40 (120 with Premium) requests per minute.
502discord_unavailableDiscord did not respond — please retry later.

The key is shown in full only once at creation — save it. The project administration may disable a key in case of abuse.

Remember to save your changes.
There are unsaved changes — apply or discard?