API Echo
Публічний REST API та вебхуки: підключайте економіку, рівні та моніторинг сервера до своїх сервісів — ігрового сервера, сайту, бота чи автоматизацій.
https://api.echo-assistants.tech
Авторизація
Кожен запит потребує ключ у заголовку Authorization. Ключ видається на сервер, а не на акаунт. Створити його може лише власник сервера у дашборді → «Розробникам»: до 3 (з Premium — 10) ключів. Ендпоінти /catalog/* — публічні, ключ не потрібен (ліміт 60/хв за IP, відповіді кешуються).
curl https://api.echo-assistants.tech/me \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Безпека ключів
Ключ починається з echoapi_, далі 30 випадкових латинських літер і цифр та 6 символів контрольної суми. Рядок із неправильною сумою API відхиляє одразу (401). Ключі попереднього вигляду (echo_…) продовжують працювати; під час перевипуску ключ отримує новий вигляд.
\bechoapi_[0-9A-Za-z]{36}\bКлюч у відкритому доступі вимикається автоматично. GitHub за програмою Secret Scanning повідомляє нам про ключі в публічних репозиторіях, а бот Echo помічає ключі в повідомленнях на серверах і видаляє такі повідомлення. Власник сервера й творець ключа отримують сповіщення на сайті, у Telegram і на пошту — з посиланням на місце знахідки.
Що робити: відкрийте ключі сервера в кабінеті розробника й натисніть Перевипустити в картці ключа. Ключ отримає новий секрет і знову запрацює, попередній перестане діяти одразу. Замініть ключ усюди, де він записаний. Увімкнути знайдений ключ без перевипуску не можна. Якщо ключ більше не потрібен, відкличте його.
Зберігайте ключ на власному сервері — у змінних оточення або сховищі секретів. Не вставляйте його в код сторінок і застосунків, у репозиторії, повідомлення та на знімки екрана.
Сервіс
/meбез скоупаПеревірка ключа: повертає сервер та його скоупи.
curl "https://api.echo-assistants.tech/me" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "guildId": "1369...", "scopes": ["levels:read"] }Економіка
/economy/balance?user=<id>economy:readБаланс учасника: гаманець, банк, фішки.
curl "https://api.echo-assistants.tech/economy/balance?user=123456789012345678" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "user": "123...", "balance": 1500, "bank": 200, "chips": 0 }/economy/balanceeconomy:writeНарахувати або списати валюту. delta може бути від’ємним; баланс залишається в діапазоні 0…2 000 000 000.
curl -X POST "https://api.echo-assistants.tech/economy/balance" \
-H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{ "user": "123...", "delta": 250 }'{ "user": "123...", "delta": 250 }{ "ok": true, "user": "123...", "balance": 1750 }/economy/leaderboard?limit=<n>economy:readТоп учасників за балансом (limit 1–100, за замовчуванням 20).
curl "https://api.echo-assistants.tech/economy/leaderboard?limit=20" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "guildId": "1369...", "leaderboard": [ { "rank": 1, "userId": "627...", "balance": 15000, "bank": 3000 } ] }Рівні
/levels/leaderboard?limit=<n>levels:readТоп учасників за досвідом (limit 1–100, за замовчуванням 20).
curl "https://api.echo-assistants.tech/levels/leaderboard?limit=20" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "guildId": "1369...", "leaderboard": [ { "rank": 1, "userId": "627...", "xp": 3965, "messages": 374, "voiceSeconds": 1273 } ] }/levels/xp?user=<id>levels:readДосвід і рівень учасника; nextLevelXp — скільки всього досвіду потрібно для наступного рівня.
curl "https://api.echo-assistants.tech/levels/xp?user=123456789012345678" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "user": "123...", "xp": 1320, "level": 4, "nextLevelXp": 1500 }/levels/xplevels:writeНарахувати або списати досвід: delta — ціле число, до ±1 000 000 за запит; досвід не опускається нижче нуля. Рівень перераховується одразу. Якщо він виріс, бот видає нагороди за рівень (ролі, монети, оголошення), як під час звичайної прокачки, і надсилає подію member.level_up з позначкою "via": "api". Зниження рівня нагород не забирає. Лише для учасників сервера (інакше 404 not_member); якщо рівні на сервері вимкнено — 409 levels_off.
curl -X POST "https://api.echo-assistants.tech/levels/xp" \
-H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{ "user": "123...", "delta": 250 }'{ "user": "123...", "delta": 250 }{ "ok": true, "user": "123...", "xp": 1570, "level": 5, "previousXp": 1320, "previousLevel": 4, "levelUp": true }Моніторинг
/monitoring/servermonitoring:readКартка сервера в каталозі: Полум’я за період, усього, місце в топі та статуси.
curl "https://api.echo-assistants.tech/monitoring/server" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "guildId": "1369...", "slug": "my-server", "flames": 420, "flamesTotal": 9100, "published": true, "rank": 3, "partner": true, "verified": true }/monitoring/top?limit=<n>monitoring:readТоп серверів каталогу за Полум’ям (limit 1–100, за замовчуванням 20).
curl "https://api.echo-assistants.tech/monitoring/top?limit=20" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "top": [ { "rank": 1, "guildId": "1369...", "slug": "my-server", "flames": 980, "flamesTotal": 21000 } ] }Інтеграції
Підключення своєї гри, сайту чи CRM: атрибути учасників стають ролями за правилами з дашборду → «Інтеграції», гравці прив'язують акаунти кодом, дозволені ролі можна видавати напряму. Тіло запису — JSON до 8 КБ.
/members/<discordId>/attributesmembers:writeПередати атрибути учасника (заміна повністю): до 25 полів, назва — латиниця/цифри/_, значення — рядок до 100 символів, число або true/false. Ролі за правилами бот перерахує одразу.
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 } }'{ "attributes": { "faction": "police", "rank": 3, "vip": true } }{ "ok": true, "discordId": "123...", "attributes": { "faction": "police", "rank": 3, "vip": true } }/members/<discordId>/attributesmembers:readЩо надіслано про учасника і який зовнішній акаунт він прив'язав.
timedRoles — ролі з дозволених для API, видані на строк, і час, коли бот їх зніме.
curl "https://api.echo-assistants.tech/members/123456789012345678/attributes" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "discordId": "123...", "externalId": "Ivan_Petrov", "attributes": { "faction": "police", "rank": 3 }, "timedRoles": [ { "roleId": "1369...", "expiresAt": "2026-10-10T12:00:00.000Z" } ] }/members/<discordId>/attributesmembers:writeЗняти всі атрибути учасника. Ролі, видані правилом, бот зніме; видані вручну — не чіпає.
curl -X DELETE "https://api.echo-assistants.tech/members/123456789012345678/attributes" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "ok": true, "removed": true }/external/<externalId>/attributesmembers:writeТе саме за ID у вашій системі (нік, UUID, license:…). Діє на того, хто прив'язав акаунт кодом; поки не прив'язано — зберігається й застосується під час прив'язки. Атрибути за Discord ID важливіші за збігу назви.
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 } }'{ "attributes": { "faction": "army", "rank": 5 } }{ "ok": true, "externalId": "Ivan_Petrov", "discordId": "123...", "attributes": { "faction": "army", "rank": 5 } }/external/<externalId>/attributesmembers:readЩо надіслано про зовнішній акаунт і хто його прив'язав (discordId: null — поки ніхто).
curl "https://api.echo-assistants.tech/external/Ivan_Petrov/attributes" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "externalId": "Ivan_Petrov", "discordId": "123...", "attributes": { "faction": "army", "rank": 5 } }/external/<externalId>/attributesmembers:writeЗняти всі атрибути зовнішнього акаунта.
curl -X DELETE "https://api.echo-assistants.tech/external/Ivan_Petrov/attributes" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "ok": true, "removed": true }/link-codeslinks:writeКод прив'язки для гравця: 8 знаків, одноразовий, діє ttlMinutes (1–60, типово 15). Гравець натискає «Прив'язати акаунт» на панелі сервера (або /link) і вводить код. Новий код того самого акаунта гасить попередній.
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 }'{ "externalId": "Ivan_Petrov", "ttlMinutes": 15 }{ "code": "K7PQ3MZA", "display": "K7PQ-3MZA", "externalId": "Ivan_Petrov", "expiresAt": "2026-09-29T12:15:00.000Z" }/links?externalId=<externalId>links:readЧий акаунт: за externalId або за discordId (?discordId=…). Немає зв'язку — 404 not_linked.
curl "https://api.echo-assistants.tech/links?externalId=Ivan_Petrov" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "externalId": "Ivan_Petrov", "discordId": "123...", "linkedAt": "2026-09-29T12:03:11.000Z" }/links?externalId=<externalId>links:writeВідв'язати акаунт (або ?discordId=…). Атрибути зовнішнього акаунта перестають діяти на людину.
curl -X DELETE "https://api.echo-assistants.tech/links?externalId=Ivan_Petrov" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "ok": true, "externalId": "Ivan_Petrov", "discordId": "123..." }/members/<discordId>/rolesroles:writeВидати й зняти ролі напряму — лише ті, що власник позначив у кабінеті розробника → «Ключі API» → «Дозволено ключам» (до 10 у кожному списку). remove знімає будь-яку дозволену роль, навіть видану вручну. Виконує бот: відповідь 202 з номером завдання.
durationMinutes (1–525 600) — видати ролі з add на строк: після його завершення бот зніме їх сам. Строк не скорочує вже призначений, а роль, яка була в учасника без строку, лишається безстроковою. add без durationMinutes робить роль безстроковою, remove скасовує призначене зняття.
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 }'{ "add": ["1369..."], "remove": ["1370..."], "durationMinutes": 1440 }{ "ok": true, "opId": 42, "status": "pending" }/role-ops/<opId>roles:writeПідсумок завдання видачі: pending, done або failed і що вийшло з кожною роллю.
У видачі на строк у result.expiresAt вказано, коли бот зніме кожну видану роль (null — роль була в учасника без строку й лишилася безстроковою).
curl "https://api.echo-assistants.tech/role-ops/42" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "opId": 42, "discordId": "123...", "status": "done", "durationMinutes": 1440, "result": { "added": ["1369..."], "removed": [], "failed": [], "expiresAt": { "1369...": "2026-10-10T12:00:00.000Z" } } }/channels/<channelId>/messagesmessages:writeПовідомлення від імені бота (власного бота сервера, якщо його підключено) — лише в канали, які власник позначив у кабінеті розробника → «Ключі API» → «Дозволено ключам» (до 10). content — до 2000 символів, embeds — до 10 простих ембедів: title, description, url, color, timestamp, author.name, footer.text, image.url, thumbnail.url, fields; посилання — лише https. Згадки за замовчуванням нікого не кличуть: покликати можна лише учасників сервера з mentionUsers (до 10), а @everyone, @here і ролі — ніколи. Тіло запиту — до 32 КБ. Виконує бот: відповідь 202 з номером завдання.
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..."] }'{ "content": "Сервер перезапустится через 5 минут", "embeds": [ { "title": "Технические работы", "description": "Начало в 03:00 МСК", "color": 9133302 } ], "mentionUsers": ["123..."] }{ "ok": true, "opId": 77, "status": "pending" }/message-ops/<opId>messages:writeПідсумок надсилання: pending, done (id повідомлення та посилання) або failed з причиною: channel_not_allowed, channel_missing, missing_permissions, rejected.
curl "https://api.echo-assistants.tech/message-ops/42" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "opId": 77, "channelId": "1371...", "status": "done", "result": { "messageId": "1372...", "channelId": "1371...", "url": "https://discord.com/channels/1369.../1371.../1372..." } }Каталог
/catalog/servers?search=&tag=&lang=&limit=публічний · без ключаПошук і список серверів у каталозі моніторингу. Усе публічно — ключ не потрібен.
curl "https://api.echo-assistants.tech/catalog/servers"
{ "count": 1, "servers": [ { "slug": "my-server", "name": "My Server", "memberCount": 5200, "flames": 980, "tags": ["Игры"], "language": "ru", "invite": "https://discord.gg/...", "badges": { "verified": true } } ] }/catalog/top?limit=<n>публічний · без ключаТоп каталогу за Полум’ям.
curl "https://api.echo-assistants.tech/catalog/top?limit=20"
{ "top": [ { "rank": 1, "slug": "my-server", "name": "My Server", "flames": 980 } ] }/catalog/server/<slug>публічний · без ключаПублічна картка сервера: опис, Полум’я, місце, теги, бейджі, інвайт, рейтинг.
curl "https://api.echo-assistants.tech/catalog/server/my-server"
{ "slug": "my-server", "name": "My Server", "flames": 980, "rank": 3, "rating": { "avg": 8.7, "count": 42 }, "invite": "https://discord.gg/..." }/catalog/reviews/<slug>?limit=<n>публічний · без ключаВідгуки про сервер із середньою оцінкою.
curl "https://api.echo-assistants.tech/catalog/reviews/my-server?limit=20"
{ "slug": "my-server", "summary": { "avg": 8.7, "count": 42 }, "reviews": [ { "user": "Ник", "rating": 9, "comment": "Топ", "at": "..." } ] }/catalog/tagsпублічний · без ключаТеги/категорії каталогу з кількістю серверів.
curl "https://api.echo-assistants.tech/catalog/tags"
{ "tags": [ { "tag": "Игры", "count": 128 } ] }Вебхуки
Echo сам надсилає події на ваш URL у реальному часі. Налаштуйте вебхук у дашборді → «Розробникам». Кожен запит підписаний заголовком X-Echo-Signature (HMAC-SHA256 тіла за вашим секретом), ім’я події — у X-Echo-Event.
flame.bumpСерверу додано Полум'я — у каталозі або командою /flame.
{
"event": "flame.bump",
"guildId": "1369...",
"flames": 421,
"flamesTotal": 9101,
"amount": 1,
"source": "web",
"at": "2026-07-27T20:30:00.000Z"
}testНадсилається кнопкою «Надіслати тест» — перевірити, що приймач працює.
{ "event": "test", "guildId": "1369...", "message": "Проверка вебхука Echo", "at": "..." }import crypto from "node:crypto";
// body — сирий текст запиту, secret — з дашборда
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);Сповіщення інтеграцій (Premium)
Налаштовуються в дашборді → «Інтеграції». Echo надсилає POST із JSON {"id", "event", "guildId", "at", "data"}: підпис — X-Echo-Signature (sha256= і HMAC-SHA256 сирого тіла за вашим секретом), назва події — X-Echo-Event, номер доставки — X-Echo-Delivery.
Відповідайте 2xx за 8 секунд. Інакше — повтор через 30 с, 2 хв, 10 хв і 1 год із тим самим id; редиректи не виконуються. Адреса — лише https на 443 і публічна IP. Після 20 невдач поспіль надсилання вимикається — увімкніть його в дашборді, коли приймач запрацює.
member.joinУчасник зайшов на сервер.
{ "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Учасник вийшов або його вигнали.
{ "id": "evt_…", "event": "member.leave", "guildId": "1369...", "at": "…", "data": { "discordId": "123...", "username": "ivan" } }member.level_upУчасник підняв рівень.
{ "id": "evt_…", "event": "member.level_up", "guildId": "1369...", "at": "…", "data": { "discordId": "123...", "oldLevel": 4, "newLevel": 5, "xp": 1320 } }store.purchaseПокупка на вітрині сервера: роль видано, пакет монет нараховано (kind "coins", поле coins) або підтримка сервера. В анонімної підтримки покупця немає.
{ "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Відкрито тікет.
{ "id": "evt_…", "event": "ticket.opened", "guildId": "1369...", "at": "…", "data": { "ticketId": 311, "number": 57, "discordId": "123...", "channelId": "1371...", "panelId": 4 } }account.linkedГравець прив'язав акаунт кодом.
{ "id": "evt_…", "event": "account.linked", "guildId": "1369...", "at": "…",
"data": { "externalId": "Ivan_Petrov", "discordId": "123...", "username": "ivan", "previousDiscordId": null, "previousExternalId": null } }testКнопка «Надіслати тест» у дашборді → «Інтеграції».
{ "id": "evt_…", "event": "test", "guildId": "1369...", "at": "…", "data": { "message": "…" } }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 часа. Несколько процессов используют одну таблицу.
// підпис рахується від сирого тіла, до розбору 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();
}
});<?php
$secret = getenv('ECHO_WEBHOOK_SECRET'); // whsec_… из панели
$guildId = getenv('ECHO_GUILD_ID');
if (!$secret || !$guildId) { http_response_code(500); exit; }
// підпис рахується від сирого тіла, до розбору 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);
}Скоупи
Під час створення ключа обираються права — кожен ендпоінт потребує свій скоуп.
guild:readeconomy:readeconomy:writelevels:readmonitoring:readmembers:readmembers:writeroles:writelinks:readlinks:writelevels:writemessages:writeЛіміти та помилки
До 3 (з Premium — 10) ключів на сервер, ліміт — 40 (з Premium — 120) запитів на хвилину на кожен ключ. Помилки приходять як JSON { "error": "..." } з відповідним HTTP-кодом.
| Код | error | Коли |
|---|---|---|
| 400 | bad_fields | Невірні або відсутні параметри запиту. |
| 401 | unauthorized | Ключ не передано, невірний або відкликаний. |
| 403 | insufficient_scope | У ключа немає потрібного скоупа для цього запиту. |
| 403 | key_paused_premium | Ключ на паузі: у сервера немає Premium, а без нього працюють лише ключі в межах безкоштовного ліміту, випущені раніше за інші. |
| 404 | not_linked | Акаунт не прив'язано (методи прив'язок) або завдання не знайдено. |
| 409 | levels_off | Дія не відповідає налаштуванням сервера: наприклад, рівні вимкнено. |
| 413 | body_too_large | Тіло запиту більше за 8 КБ. |
| 429 | rate_limited | Перевищено ліміт 40 (з Premium — 120) запитів на хвилину. |
| 502 | discord_unavailable | Discord не відповів — повторіть запит пізніше. |
Ключ видно повністю лише один раз під час створення — збережіть його. Адміністрація проєкту може вимкнути ключ у разі зловживання.