API Echo

Publiczne REST API i webhooki: podłącz ekonomię, poziomy i monitoring serwera do własnych usług — serwera gry, strony, bota albo automatyzacji.

https://api.echo-assistants.tech

Autoryzacja

Każde żądanie wymaga klucza w nagłówku Authorization. Klucz jest wydawany na serwer, a nie na konto. Utworzyć go może tylko właściciel serwera w panelu → „Dla deweloperów”: do 3 (z Premium — 10) kluczy. Endpointy /catalog/* są publiczne — klucz nie jest potrzebny (limit 60/min na IP, odpowiedzi są buforowane).

curl https://api.echo-assistants.tech/me \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Utwórz klucz

Bezpieczeństwo kluczy

Klucz zaczyna się od echoapi_, potem ma 30 losowych łacińskich liter i cyfr oraz 6-znakową sumę kontrolną. Ciąg z błędną sumą kontrolną API odrzuca od razu (401). Klucze w starym formacie (echo_…) nadal działają; klucz wygenerowany ponownie dostaje nowy format.

Wyrażenie do wyszukiwania kluczy we własnym kodzie
\bechoapi_[0-9A-Za-z]{36}\b

Klucz, który trafił do publicznego dostępu, jest wyłączany automatycznie. W ramach programu Secret Scanning GitHub zgłasza nam klucze znalezione w publicznych repozytoriach, a bot Echo wykrywa klucze w wiadomościach na serwerach i usuwa takie wiadomości. Właściciel serwera i twórca klucza dostają powiadomienie na stronie, na Telegramie i e-mailem — z linkiem do miejsca znaleziska.

Co zrobić: otwórz klucze serwera w portalu dewelopera i kliknij Wygeneruj ponownie na karcie klucza. Klucz dostanie nowy sekret i znowu zacznie działać, a stary przestanie działać od razu. Zamień klucz wszędzie, gdzie jest zapisany. Znalezionego klucza nie da się włączyć bez ponownego wygenerowania. Jeśli klucz nie jest już potrzebny, unieważnij go.

Przechowuj klucz na własnym serwerze — w zmiennych środowiskowych albo w magazynie sekretów. Nie umieszczaj go w kodzie stron i aplikacji, w repozytoriach, wiadomościach ani na zrzutach ekranu.

Usługa

GET/mebez zakresu

Sprawdzenie klucza: zwraca serwer i jego zakresy.

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

Ekonomia

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

Saldo członka: portfel, bank, żetony.

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

Dodaj lub odejmij walutę. delta może być ujemna; saldo pozostaje w zakresie 0…2 000 000 000.

Przykład
curl -X POST "https://api.echo-assistants.tech/economy/balance" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{ "user": "123...", "delta": 250 }'
Treść żądania
{ "user": "123...", "delta": 250 }
Odpowiedź
{ "ok": true, "user": "123...", "balance": 1750 }
GET/economy/leaderboard?limit=<n>economy:read

Top członków według salda (limit 1–100, domyślnie 20).

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

Poziomy

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

Top członków według doświadczenia (limit 1–100, domyślnie 20).

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

XP i poziom członka; nextLevelXp to łączna liczba XP potrzebna do następnego poziomu.

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

Dodanie lub odjęcie XP: delta to liczba całkowita, do ±1 000 000 na zapytanie; XP nie spada poniżej zera. Poziom jest od razu przeliczany. Jeśli wzrósł, bot przyznaje nagrody za poziom (role, monety, ogłoszenie) jak przy zwykłym zdobywaniu poziomów i wysyła zdarzenie member.level_up z oznaczeniem "via": "api". Obniżenie poziomu nie odbiera nagród. Tylko dla członków serwera (w przeciwnym razie 404 not_member); jeśli poziomy są wyłączone na serwerze — 409 levels_off.

Przykład
curl -X POST "https://api.echo-assistants.tech/levels/xp" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{ "user": "123...", "delta": 250 }'
Treść żądania
{ "user": "123...", "delta": 250 }
Odpowiedź
{ "ok": true, "user": "123...", "xp": 1570, "level": 5, "previousXp": 1320, "previousLevel": 4, "levelUp": true }

Monitoring

GET/monitoring/servermonitoring:read

Karta serwera w katalogu: Płomienie za okres, łącznie, miejsce w topie i statusy.

Przykład
curl "https://api.echo-assistants.tech/monitoring/server" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Odpowiedź
{ "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 serwerów katalogu według Płomieni (limit 1–100, domyślnie 20).

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

Integracje

Podłącz własną grę, stronę lub CRM: atrybuty członków zamieniają się w role według reguł z panelu → „Integracje”, gracze wiążą konta kodem, a dozwolone role można nadawać bezpośrednio. Treść żądań zapisu — JSON do 8 KB.

PUT/members/<discordId>/attributesmembers:write

Przekaż atrybuty członka (pełna zamiana): do 25 pól, nazwa — litery łacińskie/cyfry/_, wartość — tekst do 100 znaków, liczba lub true/false. Bot od razu przeliczy role według reguł.

Przykład
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 } }'
Treść żądania
{ "attributes": { "faction": "police", "rank": 3, "vip": true } }
Odpowiedź
{ "ok": true, "discordId": "123...", "attributes": { "faction": "police", "rank": 3, "vip": true } }
GET/members/<discordId>/attributesmembers:read

Co przesłano dla członka i jakie konto zewnętrzne jest z nim powiązane.

timedRoles — role dozwolone dla API, nadane na określony czas, i moment, w którym bot je zdejmie.

Przykład
curl "https://api.echo-assistants.tech/members/123456789012345678/attributes" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Odpowiedź
{ "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

Usuń wszystkie atrybuty członka. Role nadane przez regułę bot odbierze, a nadanych ręcznie nie rusza.

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

To samo według ID w twoim systemie (nick, UUID, license:…). Działa na osobę, która powiązała konto kodem; dopóki konto nie jest powiązane, atrybuty są przechowywane i zostaną zastosowane przy powiązaniu. Przy zgodnej nazwie pierwszeństwo mają atrybuty według Discord ID.

Przykład
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 } }'
Treść żądania
{ "attributes": { "faction": "army", "rank": 5 } }
Odpowiedź
{ "ok": true, "externalId": "Ivan_Petrov", "discordId": "123...", "attributes": { "faction": "army", "rank": 5 } }
GET/external/<externalId>/attributesmembers:read

Co przesłano dla konta zewnętrznego i kto je powiązał (discordId: null — jeszcze nikt).

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

Usuń wszystkie atrybuty konta zewnętrznego.

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

Kod powiązania dla gracza: 8 znaków, jednorazowy, ważny przez ttlMinutes (1–60, domyślnie 15). Gracz klika „Powiąż konto” na panelu serwera (lub używa /link) i wpisuje kod. Nowy kod dla tego samego konta unieważnia poprzedni.

Przykład
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 }'
Treść żądania
{ "externalId": "Ivan_Petrov", "ttlMinutes": 15 }
Odpowiedź
{ "code": "K7PQ3MZA", "display": "K7PQ-3MZA", "externalId": "Ivan_Petrov", "expiresAt": "2026-09-29T12:15:00.000Z" }
GET/links?externalId=<externalId>links:read

Czyje to konto: według externalId lub discordId (?discordId=…). Brak powiązania — 404 not_linked.

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

Odłącz konto (lub ?discordId=…). Atrybuty konta zewnętrznego przestają działać na tę osobę.

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

Nadawaj i odbieraj role bezpośrednio — tylko te, które właściciel oznaczył w portalu deweloperów → „Klucze API” → „Dozwolone dla kluczy” (do 10 na każdej liście). remove odbiera każdą dozwoloną rolę, nawet nadaną ręcznie. Wykonuje to bot: odpowiedź 202 z numerem zadania.

durationMinutes (1–525 600) — nadaj role z add na określony czas: po jego upływie bot sam je zdejmie. Termin nigdy nie skraca już ustawionego, a rola, którą członek miał bezterminowo, zostaje na stałe. add bez durationMinutes nadaje rolę na stałe, remove anuluje zaplanowane zdjęcie.

Przykład
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 }'
Treść żądania
{ "add": ["1369..."], "remove": ["1370..."], "durationMinutes": 1440 }
Odpowiedź
{ "ok": true, "opId": 42, "status": "pending" }
GET/role-ops/<opId>roles:write

Wynik zadania nadawania: pending, done lub failed oraz co się stało z każdą rolą.

Przy nadaniu na czas result.expiresAt pokazuje, kiedy bot zdejmie każdą nadaną rolę (null — członek miał już tę rolę bezterminowo i zostaje ona na stałe).

Przykład
curl "https://api.echo-assistants.tech/role-ops/42" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Odpowiedź
{ "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

Wiadomość w imieniu bota (własnego bota serwera, jeśli jest podłączony) — tylko w kanałach, które właściciel oznaczył w portalu deweloperów → „Klucze API” → „Dozwolone dla kluczy” (do 10). content — do 2000 znaków, embeds — do 10 prostych embedów: title, description, url, color, timestamp, author.name, footer.text, image.url, thumbnail.url, fields; linki tylko https. Wzmianki domyślnie nikogo nie powiadamiają: powiadomić można tylko członków serwera z mentionUsers (do 10), a @everyone, @here i role nigdy. Treść żądania — do 32 KB. Wykonuje to bot: odpowiedź 202 z numerem zadania.

Przykład
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..."] }'
Treść żądania
{ "content": "Сервер перезапустится через 5 минут", "embeds": [ { "title": "Технические работы", "description": "Начало в 03:00 МСК", "color": 9133302 } ], "mentionUsers": ["123..."] }
Odpowiedź
{ "ok": true, "opId": 77, "status": "pending" }
GET/message-ops/<opId>messages:write

Wynik wysyłki: pending, done (id wiadomości i link) albo failed z powodem: channel_not_allowed, channel_missing, missing_permissions, rejected.

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

Katalog

GET/catalog/servers?search=&tag=&lang=&limit=publiczny · bez klucza

Wyszukiwanie i lista serwerów w katalogu monitoringu. Wszystko publiczne — klucz nie jest potrzebny.

Przykład
curl "https://api.echo-assistants.tech/catalog/servers"
Odpowiedź
{ "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>publiczny · bez klucza

Top katalogu według Płomieni.

Przykład
curl "https://api.echo-assistants.tech/catalog/top?limit=20"
Odpowiedź
{ "top": [ { "rank": 1, "slug": "my-server", "name": "My Server", "flames": 980 } ] }
GET/catalog/server/<slug>publiczny · bez klucza

Publiczna karta serwera: opis, Płomienie, miejsce, tagi, odznaki, zaproszenie, ocena.

Przykład
curl "https://api.echo-assistants.tech/catalog/server/my-server"
Odpowiedź
{ "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>publiczny · bez klucza

Opinie o serwerze ze średnią oceną.

Przykład
curl "https://api.echo-assistants.tech/catalog/reviews/my-server?limit=20"
Odpowiedź
{ "slug": "my-server", "summary": { "avg": 8.7, "count": 42 }, "reviews": [ { "user": "Ник", "rating": 9, "comment": "Топ", "at": "..." } ] }
GET/catalog/tagspubliczny · bez klucza

Tagi/kategorie katalogu z liczbą serwerów.

Przykład
curl "https://api.echo-assistants.tech/catalog/tags"
Odpowiedź
{ "tags": [ { "tag": "Игры", "count": 128 } ] }

Webhooki

Echo samo wysyła zdarzenia na twój URL w czasie rzeczywistym. Skonfiguruj webhook w panelu → „Dla deweloperów”. Każde żądanie jest podpisane nagłówkiem X-Echo-Signature (HMAC-SHA256 treści z użyciem twojego sekretu), a nazwa zdarzenia jest w X-Echo-Event.

flame.bump

Serwer otrzymał Płomień — w katalogu lub komendą /flame.

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

Wysyłane przyciskiem „Wyślij test” — by sprawdzić, czy odbiornik działa.

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

// body — surowa treść żądania, secret — z panelu
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);

Powiadomienia integracji (Premium)

Konfiguruje się je w panelu → „Integracje”. Echo wysyła POST z JSON-em {"id", "event", "guildId", "at", "data"}: podpis — X-Echo-Signature (sha256= i HMAC-SHA256 surowej treści z użyciem twojego sekretu), nazwa zdarzenia — X-Echo-Event, numer dostarczenia — X-Echo-Delivery.

Odpowiedz kodem 2xx w ciągu 8 sekund. W przeciwnym razie — ponowienie po 30 s, 2 min, 10 min i 1 godz. z tym samym id; przekierowania nie są obsługiwane. Adres — tylko https na porcie 443 i publiczne IP. Po 20 nieudanych próbach z rzędu wysyłanie zostaje wyłączone — włącz je w panelu, gdy odbiornik zacznie działać.

member.join

Członek dołączył do serwera.

{ "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

Członek wyszedł lub został wyrzucony.

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

Członek awansował na wyższy poziom.

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

Zakup w witrynie serwera: rola nadana, pakiet monet dodany (kind "coins", pole coins) lub wsparcie serwera. Anonimowe wsparcie nie ma kupującego.

{ "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

Otwarto ticket.

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

Gracz powiązał konto kodem.

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

Przycisk „Wyślij test” w panelu → „Integracje”.

{ "id": "evt_…", "event": "test", "guildId": "1369...", "at": "…", "data": { "message": "…" } }
Weryfikacja podpisu — 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 часа. Несколько процессов используют одну таблицу.

// podpis liczony jest z surowej treści, przed parsowaniem 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();
  }
});
Weryfikacja podpisu — PHP
<?php
$secret = getenv('ECHO_WEBHOOK_SECRET'); // whsec_… из панели
$guildId = getenv('ECHO_GUILD_ID');
if (!$secret || !$guildId) { http_response_code(500); exit; }
// podpis liczony jest z surowej treści, przed parsowaniem 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);
}

Zakresy

Przy tworzeniu klucza wybierasz uprawnienia — każdy endpoint wymaga własnego zakresu.

guild:read
Dane serwera
Podstawowe informacje o serwerze.
economy:read
Odczyt ekonomii
Salda: portfel, bank, żetony.
economy:write
Zapis ekonomii
Dodawanie i odejmowanie waluty.
levels:read
Odczyt poziomów
Doświadczenie i ranking członków.
monitoring:read
Odczyt monitoringu
Płomienie, miejsce w topie, statusy w katalogu.
members:read
Odczyt atrybutów
Odczyt atrybutów członków według Discord ID i ID w systemie zewnętrznym — bez prawa do ich zmiany.
members:write
Atrybuty członków
Przekazywanie atrybutów według Discord ID i według ID w systemie zewnętrznym.
roles:write
Nadawanie ról
Nadawanie i odbieranie ról dozwolonych w portalu deweloperów → „Klucze API” → „Dozwolone dla kluczy”.
links:read
Odczyt powiązań
Czyje to konto: powiązanie zewnętrznego ID z Discordem.
links:write
Kody powiązania
Wydawanie kodów powiązania i odłączanie kont.
levels:write
Zapis XP
Dodawanie i odejmowanie XP; nagrody za poziom przyznaje bot.
messages:write
Wiadomości od bota
Wiadomości w imieniu bota w kanałach dozwolonych w portalu deweloperów → „Klucze API” → „Dozwolone dla kluczy”.

Limity i błędy

Do 3 (z Premium — 10) kluczy na serwer, limit żądań na minutę dla każdego klucza: 40 (z Premium — 120). Błędy są zwracane jako JSON { "error": "..." } z odpowiednim kodem HTTP.

KoderrorKiedy
400bad_fieldsNieprawidłowe lub brakujące parametry żądania.
401unauthorizedKlucz nie został przekazany, jest nieprawidłowy lub unieważniony.
403insufficient_scopeKlucz nie ma zakresu wymaganego dla tego żądania.
403key_paused_premiumKlucz jest wstrzymany: serwer nie ma Premium, a bez niego działają tylko klucze w ramach darmowego limitu, wygenerowane jako pierwsze.
404not_linkedKonto nie jest powiązane (metody powiązań) lub nie znaleziono zadania.
409levels_offDziałanie nie pasuje do ustawień serwera: na przykład poziomy są wyłączone.
413body_too_largeTreść żądania przekracza 8 KB.
429rate_limitedPrzekroczono limit żądań na minutę (40 (z Premium — 120)).
502discord_unavailableDiscord nie odpowiedział — powtórz zapytanie później.

Pełny klucz widać tylko raz, przy tworzeniu — zapisz go. Administracja projektu może wyłączyć klucz w razie nadużyć.

Pamiętaj, aby zapisać zmiany.
Masz niezapisane zmiany — zastosować czy odrzucić?