API Echo

Öffentliche REST-API und Webhooks: Verbinde Wirtschaft, Level und Monitoring deines Servers mit deinen eigenen Diensten — Gameserver, Website, Bot oder Automatisierungen.

https://api.echo-assistants.tech

Autorisierung

Jede Anfrage benötigt einen Schlüssel im Authorization-Header. Ein Schlüssel wird für einen einen Server ausgestellt, nicht für ein Konto. Nur der Serverinhaber kann ihn im Dashboard → „Entwickler“ erstellen: bis zu 3 (10 mit Premium) Schlüssel. Die /catalog/*-Endpunkte sind öffentlich — kein Schlüssel nötig (Limit 60/Min. pro IP, Antworten werden gecacht).

curl https://api.echo-assistants.tech/me \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Schlüssel erstellen

Schlüsselsicherheit

Ein Schlüssel beginnt mit echoapi_, danach folgen 30 zufällige lateinische Buchstaben und Ziffern und eine Prüfsumme aus 6 Zeichen. Eine Zeichenfolge mit falscher Prüfsumme lehnt die API sofort ab (401). Schlüssel im alten Format (echo_…) funktionieren weiter; beim Neuausstellen erhält der Schlüssel das neue Format.

Ausdruck zum Suchen von Schlüsseln im eigenen Code
\bechoapi_[0-9A-Za-z]{36}\b

Ein öffentlich gewordener Schlüssel wird automatisch deaktiviert. Über das Programm Secret Scanning meldet uns GitHub Schlüssel in öffentlichen Repositories, und der Echo-Bot erkennt Schlüssel in Nachrichten auf Servern und löscht diese Nachrichten. Der Server-Besitzer und der Ersteller des Schlüssels werden auf der Website, in Telegram und per E-Mail benachrichtigt — mit einem Link zur Fundstelle.

Was zu tun ist: Öffnen Sie die Schlüssel des Servers im Entwicklerportal und klicken Sie auf der Karte des Schlüssels auf Neu ausstellen. Der Schlüssel erhält ein neues Geheimnis und funktioniert wieder, das bisherige gilt sofort nicht mehr. Ersetzen Sie den Schlüssel überall, wo er hinterlegt ist. Ohne Neuausstellung lässt sich ein gefundener Schlüssel nicht wieder aktivieren. Wenn Sie ihn nicht mehr brauchen, widerrufen Sie ihn.

Bewahren Sie den Schlüssel auf Ihrem eigenen Server auf — in Umgebungsvariablen oder einem Geheimnisspeicher. Fügen Sie ihn nicht in Seiten- oder App-Code, Repositories, Nachrichten oder Screenshots ein.

Service

GET/mekein Scope

Schlüsselprüfung: gibt den Server und seine Scopes zurück.

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

Wirtschaft

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

Guthaben des Mitglieds: Wallet, Bank, Chips.

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

Währung gutschreiben oder abziehen. delta kann negativ sein; das Guthaben bleibt im Bereich 0…2 000 000 000.

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

Top-Mitglieder nach Guthaben (limit 1–100, Standard 20).

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

Level

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

Top-Mitglieder nach Erfahrung (limit 1–100, Standard 20).

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

EP und Level eines Mitglieds; nextLevelXp ist die gesamte EP, die für das nächste Level nötig ist.

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

EP gutschreiben oder abziehen: delta ist eine ganze Zahl, bis zu ±1.000.000 pro Anfrage; EP fallen nie unter null. Das Level wird sofort neu berechnet. Steigt es, vergibt der Bot die Levelbelohnungen (Rollen, Münzen, Ankündigung) wie beim normalen Leveln und sendet das Ereignis member.level_up mit der Kennzeichnung "via": "api". Ein Levelabstieg nimmt keine Belohnungen zurück. Nur für Mitglieder des Servers (sonst 404 not_member); sind Level auf dem Server ausgeschaltet — 409 levels_off.

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

Monitoring

GET/monitoring/servermonitoring:read

Serverkarte im Katalog: Flammen im Zeitraum, gesamt, Top-Platz und Status.

Beispiel
curl "https://api.echo-assistants.tech/monitoring/server" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Antwort
{ "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-Katalogserver nach Flammen (limit 1–100, Standard 20).

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

Integrationen

Verbinde dein Spiel, deine Website oder dein CRM: Mitgliederattribute werden nach den Regeln im Dashboard → „Integrationen“ zu Rollen, Spieler verknüpfen Konten per Code, und erlaubte Rollen lassen sich direkt vergeben. Schreibanfragen sind JSON bis 8 KB.

PUT/members/<discordId>/attributesmembers:write

Attribute eines Mitglieds senden (vollständiger Ersatz): bis zu 25 Felder, Namen aus lateinischen Buchstaben/Ziffern/_, Werte als Text bis 100 Zeichen, Zahl oder true/false. Der Bot berechnet die Regelrollen sofort neu.

Beispiel
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 } }'
Anfrage-Body
{ "attributes": { "faction": "police", "rank": 3, "vip": true } }
Antwort
{ "ok": true, "discordId": "123...", "attributes": { "faction": "police", "rank": 3, "vip": true } }
GET/members/<discordId>/attributesmembers:read

Was für das Mitglied gesendet wurde und welches externe Konto verknüpft ist.

timedRoles — für die API erlaubte Rollen, die befristet vergeben wurden, und wann der Bot sie entfernt.

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

Alle Attribute eines Mitglieds entfernen. Der Bot entzieht von Regeln vergebene Rollen; manuell vergebene bleiben.

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

Dasselbe über die ID in deinem System (Nickname, UUID, license:…). Gilt für die Person, die das Konto per Code verknüpft hat; bis dahin gespeichert und bei Verknüpfung angewendet. Attribute per Discord-ID haben bei gleichem Namen Vorrang.

Beispiel
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 } }'
Anfrage-Body
{ "attributes": { "faction": "army", "rank": 5 } }
Antwort
{ "ok": true, "externalId": "Ivan_Petrov", "discordId": "123...", "attributes": { "faction": "army", "rank": 5 } }
GET/external/<externalId>/attributesmembers:read

Was für das externe Konto gesendet wurde und wer es verknüpft hat (discordId: null — noch niemand).

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

Alle Attribute eines externen Kontos entfernen.

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

Ein Verknüpfungscode für einen Spieler: 8 Zeichen, einmalig, gültig für ttlMinutes (1–60, Standard 15). Der Spieler drückt „Konto verknüpfen“ im Serverpanel (oder nutzt /link) und gibt ihn ein. Ein neuer Code für dasselbe Konto macht den alten ungültig.

Beispiel
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 }'
Anfrage-Body
{ "externalId": "Ivan_Petrov", "ttlMinutes": 15 }
Antwort
{ "code": "K7PQ3MZA", "display": "K7PQ-3MZA", "externalId": "Ivan_Petrov", "expiresAt": "2026-09-29T12:15:00.000Z" }
GET/links?externalId=<externalId>links:read

Wessen Konto: per externalId oder discordId (?discordId=…). Keine Verknüpfung — 404 not_linked.

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

Konto trennen (oder ?discordId=…). Die Attribute des externen Kontos gelten nicht mehr für die Person.

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

Rollen direkt vergeben und entziehen — nur die, die der Inhaber im Entwicklerportal → „API-Schlüssel“ → „Für Schlüssel erlaubt“ markiert hat (bis zu 10 pro Liste). remove entzieht jede erlaubte Rolle, auch eine manuell vergebene. Der Bot führt es aus: Antwort 202 mit Auftragsnummer.

durationMinutes (1–525.600) — die Rollen aus add befristet vergeben: nach Ablauf entfernt der Bot sie selbst. Die Frist verkürzt nie eine bereits gesetzte, und eine Rolle, die das Mitglied schon unbefristet hatte, bleibt unbefristet. add ohne durationMinutes macht die Rolle unbefristet, remove hebt eine geplante Entfernung auf.

Beispiel
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 }'
Anfrage-Body
{ "add": ["1369..."], "remove": ["1370..."], "durationMinutes": 1440 }
Antwort
{ "ok": true, "opId": 42, "status": "pending" }
GET/role-ops/<opId>roles:write

Ergebnis eines Vergabeauftrags: pending, done oder failed und was mit jeder Rolle passiert ist.

Bei befristeter Vergabe steht in result.expiresAt, wann der Bot jede vergebene Rolle entfernt (null — das Mitglied hatte die Rolle bereits unbefristet, sie bleibt unbefristet).

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

Eine Nachricht im Namen des Bots (des eigenen Bots des Servers, falls verbunden) — nur in Kanäle, die der Inhaber im Entwicklerportal → „API-Schlüssel“ → „Für Schlüssel erlaubt“ markiert hat (bis zu 10). content — bis zu 2000 Zeichen, embeds — bis zu 10 einfache Embeds: title, description, url, color, timestamp, author.name, footer.text, image.url, thumbnail.url, fields; Links nur mit https. Erwähnungen benachrichtigen standardmäßig niemanden: benachrichtigt werden nur Servermitglieder aus mentionUsers (bis zu 10), @everyone, @here und Rollen nie. Anfragekörper — bis zu 32 KB. Der Bot führt es aus: Antwort 202 mit Auftragsnummer.

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

Ergebnis des Versands: pending, done (Nachrichten-ID und Link) oder failed mit Grund: channel_not_allowed, channel_missing, missing_permissions, rejected.

Beispiel
curl "https://api.echo-assistants.tech/message-ops/42" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Antwort
{ "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=öffentlich · kein Schlüssel

Server im Monitoring-Katalog suchen und auflisten. Vollständig öffentlich — kein Schlüssel nötig.

Beispiel
curl "https://api.echo-assistants.tech/catalog/servers"
Antwort
{ "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>öffentlich · kein Schlüssel

Katalog-Top nach Flammen.

Beispiel
curl "https://api.echo-assistants.tech/catalog/top?limit=20"
Antwort
{ "top": [ { "rank": 1, "slug": "my-server", "name": "My Server", "flames": 980 } ] }
GET/catalog/server/<slug>öffentlich · kein Schlüssel

Öffentliche Serverkarte: Beschreibung, Flammen, Platz, Tags, Badges, Invite, Bewertung.

Beispiel
curl "https://api.echo-assistants.tech/catalog/server/my-server"
Antwort
{ "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>öffentlich · kein Schlüssel

Serverbewertungen mit Durchschnittswertung.

Beispiel
curl "https://api.echo-assistants.tech/catalog/reviews/my-server?limit=20"
Antwort
{ "slug": "my-server", "summary": { "avg": 8.7, "count": 42 }, "reviews": [ { "user": "Ник", "rating": 9, "comment": "Топ", "at": "..." } ] }
GET/catalog/tagsöffentlich · kein Schlüssel

Katalog-Tags/-Kategorien mit Serveranzahl.

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

Webhooks

Echo sendet Ereignisse in Echtzeit an deine URL. Richte einen Webhook im Dashboard → „Entwickler“ ein. Jede Anfrage ist mit dem X-Echo-Signature-Header signiert (HMAC-SHA256 des Bodys mit deinem Secret); der Ereignisname steht in X-Echo-Event.

flame.bump

Der Server hat Flamme erhalten – im Katalog oder per /flame.

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

Wird per Schaltfläche „Test senden“ gesendet — um zu prüfen, ob der Empfänger funktioniert.

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

// body ist der rohe Anfragetext, secret stammt aus dem 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);

Integrations-Benachrichtigungen (Premium)

Eingerichtet im Dashboard → „Integrationen“. Echo sendet einen JSON-POST {"id", "event", "guildId", "at", "data"}: Die Signatur steht in X-Echo-Signature (sha256= plus HMAC-SHA256 des Rohinhalts mit deinem Geheimnis), der Ereignisname in X-Echo-Event, die Zustellnummer in X-Echo-Delivery.

Antworte innerhalb von 8 Sekunden mit 2xx. Sonst wiederholen wir nach 30 s, 2 Min, 10 Min und 1 Std mit derselben id; Weiterleitungen werden nicht verfolgt. Die URL muss https auf 443 mit öffentlicher IP sein. Nach 20 Fehlschlägen in Folge wird das Senden abgeschaltet — schalte es im Dashboard wieder ein, sobald dein Empfänger läuft.

member.join

Ein Mitglied ist dem Server beigetreten.

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

Ein Mitglied hat den Server verlassen oder wurde entfernt.

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

Ein Mitglied ist aufgestiegen.

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

Ein Kauf im Server-Shop: Rolle vergeben, Münzpaket gutgeschrieben (kind "coins", Feld coins) oder eine Serverspende. Anonyme Spenden haben keinen Käufer.

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

Ein Ticket wurde geöffnet.

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

Ein Spieler hat ein Konto per Code verknüpft.

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

Der Button „Test senden“ im Dashboard → „Integrationen“.

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

// die Signatur wird über den Rohinhalt berechnet, vor dem JSON-Parsen
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();
  }
});
Signaturprüfung — PHP
<?php
$secret = getenv('ECHO_WEBHOOK_SECRET'); // whsec_… из панели
$guildId = getenv('ECHO_GUILD_ID');
if (!$secret || !$guildId) { http_response_code(500); exit; }
// die Signatur wird über den Rohinhalt berechnet, vor dem JSON-Parsen
$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

Beim Erstellen eines Schlüssels wählst du Rechte — jeder Endpunkt erfordert seinen eigenen Scope.

guild:read
Serverdaten
Grundlegende Informationen über den Server.
economy:read
Wirtschaft lesen
Guthaben: Wallet, Bank, Chips.
economy:write
Wirtschaft schreiben
Währung gutschreiben und abziehen.
levels:read
Level lesen
Erfahrung und Mitglieder-Rangliste.
monitoring:read
Monitoring lesen
Flammen, Top-Platz, Katalog-Status.
members:read
Attribute lesen
Mitgliederattribute per Discord-ID und per externer System-ID lesen – ohne Recht, sie zu ändern.
members:write
Mitgliederattribute
Attribute per Discord-ID und per externer System-ID senden.
roles:write
Rollenvergabe
Rollen vergeben und entziehen, die im Entwicklerportal → „API-Schlüssel“ → „Für Schlüssel erlaubt“ erlaubt sind.
links:read
Verknüpfungen lesen
Wessen Konto: Verknüpfung externe ID ↔ Discord.
links:write
Verknüpfungscodes
Verknüpfungscodes ausgeben und Konten trennen.
levels:write
EP schreiben
EP gutschreiben und abziehen; Levelbelohnungen vergibt der Bot.
messages:write
Nachrichten vom Bot
Nachrichten im Namen des Bots in Kanäle, die im Entwicklerportal → „API-Schlüssel“ → „Für Schlüssel erlaubt“ erlaubt sind.

Limits und Fehler

Bis zu 3 (10 mit Premium) Schlüssel pro Server, Limit 40 (120 mit Premium) Anfragen pro Minute je Schlüssel. Fehler kommen als JSON { "error": "..." } mit dem entsprechenden HTTP-Code.

CodeerrorWann
400bad_fieldsUngültige oder fehlende Anfrageparameter.
401unauthorizedSchlüssel nicht übergeben, ungültig oder widerrufen.
403insufficient_scopeDem Schlüssel fehlt der erforderliche Scope für diese Anfrage.
403key_paused_premiumDer Schlüssel ist pausiert: Der Server hat kein Premium, und ohne Premium funktionieren nur die zuerst ausgegebenen Schlüssel innerhalb des kostenlosen Limits.
404not_linkedDas Konto ist nicht verknüpft (Verknüpfungsmethoden) oder der Auftrag wurde nicht gefunden.
409levels_offDie Aktion passt nicht zu den Servereinstellungen: zum Beispiel sind Level ausgeschaltet.
413body_too_largeDer Anfrageinhalt ist größer als 8 KB.
429rate_limitedLimit von 40 (120 mit Premium) Anfragen pro Minute überschritten.
502discord_unavailableDiscord hat nicht geantwortet — wiederholen Sie die Anfrage später.

Der Schlüssel wird nur einmal bei der Erstellung vollständig angezeigt — speichere ihn. Die Projektadministration kann einen Schlüssel bei Missbrauch deaktivieren.

Vergessen Sie nicht, die Änderungen zu speichern.
Es gibt ungespeicherte Änderungen — übernehmen oder verwerfen?