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ü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.
\bechoapi_[0-9A-Za-z]{36}\bEin ö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
/mekein ScopeSchlüsselprüfung: gibt den Server und seine Scopes zurück.
curl "https://api.echo-assistants.tech/me" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "guildId": "1369...", "scopes": ["levels:read"] }Wirtschaft
/economy/balance?user=<id>economy:readGuthaben des Mitglieds: Wallet, Bank, Chips.
curl "https://api.echo-assistants.tech/economy/balance?user=123456789012345678" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "user": "123...", "balance": 1500, "bank": 200, "chips": 0 }/economy/balanceeconomy:writeWährung gutschreiben oder abziehen. delta kann negativ sein; das Guthaben bleibt im Bereich 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:readTop-Mitglieder nach Guthaben (limit 1–100, Standard 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 } ] }Level
/levels/leaderboard?limit=<n>levels:readTop-Mitglieder nach Erfahrung (limit 1–100, Standard 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:readEP und Level eines Mitglieds; nextLevelXp ist die gesamte EP, die für das nächste Level nötig ist.
curl "https://api.echo-assistants.tech/levels/xp?user=123456789012345678" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "user": "123...", "xp": 1320, "level": 4, "nextLevelXp": 1500 }/levels/xplevels:writeEP 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.
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
/monitoring/servermonitoring:readServerkarte im Katalog: Flammen im Zeitraum, gesamt, Top-Platz und Status.
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:readTop-Katalogserver nach Flammen (limit 1–100, Standard 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 } ] }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.
/members/<discordId>/attributesmembers:writeAttribute 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.
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:readWas 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.
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:writeAlle Attribute eines Mitglieds entfernen. Der Bot entzieht von Regeln vergebene Rollen; manuell vergebene bleiben.
curl -X DELETE "https://api.echo-assistants.tech/members/123456789012345678/attributes" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "ok": true, "removed": true }/external/<externalId>/attributesmembers:writeDasselbe ü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.
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:readWas für das externe Konto gesendet wurde und wer es verknüpft hat (discordId: null — noch niemand).
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:writeAlle Attribute eines externen Kontos entfernen.
curl -X DELETE "https://api.echo-assistants.tech/external/Ivan_Petrov/attributes" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "ok": true, "removed": true }/link-codeslinks:writeEin 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.
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:readWessen Konto: per externalId oder discordId (?discordId=…). Keine Verknüpfung — 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:writeKonto trennen (oder ?discordId=…). Die Attribute des externen Kontos gelten nicht mehr für die Person.
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:writeRollen 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.
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:writeErgebnis 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).
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:writeEine 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.
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:writeErgebnis des Versands: pending, done (Nachrichten-ID und Link) oder failed mit Grund: 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..." } }Katalog
/catalog/servers?search=&tag=&lang=&limit=öffentlich · kein SchlüsselServer im Monitoring-Katalog suchen und auflisten. Vollständig öffentlich — kein Schlüssel nötig.
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>öffentlich · kein SchlüsselKatalog-Top nach Flammen.
curl "https://api.echo-assistants.tech/catalog/top?limit=20"
{ "top": [ { "rank": 1, "slug": "my-server", "name": "My Server", "flames": 980 } ] }/catalog/server/<slug>öffentlich · kein SchlüsselÖffentliche Serverkarte: Beschreibung, Flammen, Platz, Tags, Badges, Invite, Bewertung.
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>öffentlich · kein SchlüsselServerbewertungen mit Durchschnittswertung.
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öffentlich · kein SchlüsselKatalog-Tags/-Kategorien mit Serveranzahl.
curl "https://api.echo-assistants.tech/catalog/tags"
{ "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.bumpDer 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"
}testWird per Schaltfläche „Test senden“ gesendet — um zu prüfen, ob der Empfänger funktioniert.
{ "event": "test", "guildId": "1369...", "message": "Проверка вебхука Echo", "at": "..." }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.joinEin 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.leaveEin Mitglied hat den Server verlassen oder wurde entfernt.
{ "id": "evt_…", "event": "member.leave", "guildId": "1369...", "at": "…", "data": { "discordId": "123...", "username": "ivan" } }member.level_upEin Mitglied ist aufgestiegen.
{ "id": "evt_…", "event": "member.level_up", "guildId": "1369...", "at": "…", "data": { "discordId": "123...", "oldLevel": 4, "newLevel": 5, "xp": 1320 } }store.purchaseEin 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.openedEin Ticket wurde geöffnet.
{ "id": "evt_…", "event": "ticket.opened", "guildId": "1369...", "at": "…", "data": { "ticketId": 311, "number": 57, "discordId": "123...", "channelId": "1371...", "panelId": 4 } }account.linkedEin 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 } }testDer Button „Test senden“ im Dashboard → „Integrationen“.
{ "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 часа. Несколько процессов используют одну таблицу.
// 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();
}
});<?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:readeconomy:readeconomy:writelevels:readmonitoring:readmembers:readmembers:writeroles:writelinks:readlinks:writelevels:writemessages:writeLimits 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.
| Code | error | Wann |
|---|---|---|
| 400 | bad_fields | Ungültige oder fehlende Anfrageparameter. |
| 401 | unauthorized | Schlüssel nicht übergeben, ungültig oder widerrufen. |
| 403 | insufficient_scope | Dem Schlüssel fehlt der erforderliche Scope für diese Anfrage. |
| 403 | key_paused_premium | Der Schlüssel ist pausiert: Der Server hat kein Premium, und ohne Premium funktionieren nur die zuerst ausgegebenen Schlüssel innerhalb des kostenlosen Limits. |
| 404 | not_linked | Das Konto ist nicht verknüpft (Verknüpfungsmethoden) oder der Auftrag wurde nicht gefunden. |
| 409 | levels_off | Die Aktion passt nicht zu den Servereinstellungen: zum Beispiel sind Level ausgeschaltet. |
| 413 | body_too_large | Der Anfrageinhalt ist größer als 8 KB. |
| 429 | rate_limited | Limit von 40 (120 mit Premium) Anfragen pro Minute überschritten. |
| 502 | discord_unavailable | Discord 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.