API Echo
API REST pública y webhooks: conecta la economía, los niveles y el monitoreo de tu servidor con tus propios servicios: un servidor de juego, una web, un bot o automatizaciones.
https://api.echo-assistants.tech
Autorización
Cada solicitud necesita una clave en la cabecera Authorization. La clave se emite para un un servidor, no para una cuenta. Solo el propietario del servidor puede crearla en el panel → «Desarrolladores»: hasta 3 (10 con Premium) claves. Los endpoints /catalog/* son públicos: no se necesita clave (límite de 60/min por IP, las respuestas se almacenan en caché).
curl https://api.echo-assistants.tech/me \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Seguridad de las claves
La clave empieza por echoapi_, seguida de 30 letras latinas y cifras aleatorias y una suma de control de 6 caracteres. Una cadena con la suma incorrecta la rechaza la API de inmediato (401). Las claves del formato anterior (echo_…) siguen funcionando; al reemitirla, la clave recibe el formato nuevo.
\bechoapi_[0-9A-Za-z]{36}\bUna clave expuesta públicamente se desactiva automáticamente. Mediante el programa Secret Scanning, GitHub nos avisa de las claves en repositorios públicos, y el bot de Echo detecta claves en los mensajes de los servidores y borra esos mensajes. El dueño del servidor y el creador de la clave reciben un aviso en el sitio, en Telegram y por correo, con un enlace al lugar donde se encontró.
Qué hacer: abra las claves del servidor en el portal para desarrolladores y pulse Reemitir en la tarjeta de la clave. La clave recibirá un secreto nuevo y volverá a funcionar; el anterior dejará de servir de inmediato. Sustituya la clave en todos los sitios donde esté guardada. Una clave encontrada no se puede volver a activar sin reemitirla. Si ya no la necesita, revóquela.
Guarde la clave en su propio servidor: en variables de entorno o en un almacén de secretos. No la ponga en el código de páginas o aplicaciones, en repositorios, mensajes ni capturas de pantalla.
Servicio
/mesin scopeComprobación de clave: devuelve el servidor y sus scopes.
curl "https://api.echo-assistants.tech/me" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "guildId": "1369...", "scopes": ["levels:read"] }Economía
/economy/balance?user=<id>economy:readSaldo del miembro: cartera, banco, fichas.
curl "https://api.echo-assistants.tech/economy/balance?user=123456789012345678" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "user": "123...", "balance": 1500, "bank": 200, "chips": 0 }/economy/balanceeconomy:writeAñadir o restar moneda. delta puede ser negativo; el saldo se mantiene en el rango 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 de miembros por saldo (limit 1–100, por defecto 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 } ] }Niveles
/levels/leaderboard?limit=<n>levels:readTop de miembros por experiencia (limit 1–100, por defecto 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:readLa experiencia y el nivel de un miembro; nextLevelXp es la experiencia total necesaria para el siguiente nivel.
curl "https://api.echo-assistants.tech/levels/xp?user=123456789012345678" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "user": "123...", "xp": 1320, "level": 4, "nextLevelXp": 1500 }/levels/xplevels:writeSumar o restar experiencia: delta es un número entero, hasta ±1 000 000 por solicitud; la experiencia nunca baja de cero. El nivel se recalcula al instante. Si sube, el bot entrega las recompensas de nivel (roles, monedas, anuncio) como en la subida normal y envía el evento member.level_up con la marca "via": "api". Bajar de nivel no retira recompensas. Solo para miembros del servidor (si no, 404 not_member); si los niveles están desactivados en el servidor — 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 }Monitoreo
/monitoring/servermonitoring:readFicha del servidor en el catálogo: Llamas del período, total, puesto y estados.
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 de servidores del catálogo por Llamas (limit 1–100, por defecto 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 } ] }Integraciones
Conecta tu juego, web o CRM: los atributos de los miembros se convierten en roles según las reglas del panel → «Integraciones», los jugadores vinculan cuentas con un código y los roles permitidos se pueden asignar directamente. Los cuerpos de escritura son JSON de hasta 8 KB.
/members/<discordId>/attributesmembers:writeEnviar los atributos de un miembro (reemplazo completo): hasta 25 campos, nombres con letras latinas/dígitos/_, valores de texto de hasta 100 caracteres, número o true/false. El bot recalcula los roles de las reglas al instante.
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:readLo enviado para el miembro y qué cuenta externa vinculó.
timedRoles — roles permitidos para la API que se dieron por un tiempo, y cuándo el bot los quitará.
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:writeBorrar todos los atributos de un miembro. El bot quita los roles dados por reglas y deja los dados a mano.
curl -X DELETE "https://api.echo-assistants.tech/members/123456789012345678/attributes" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "ok": true, "removed": true }/external/<externalId>/attributesmembers:writeLo mismo por el ID de tu sistema (apodo, UUID, license:…). Se aplica a quien vinculó la cuenta con un código; hasta entonces se guarda y se aplica al vincular. Los atributos por Discord ID ganan si coinciden los nombres.
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:readLo enviado para la cuenta externa y quién la vinculó (discordId: null — nadie aún).
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:writeBorrar todos los atributos de una cuenta externa.
curl -X DELETE "https://api.echo-assistants.tech/external/Ivan_Petrov/attributes" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "ok": true, "removed": true }/link-codeslinks:writeUn código de vinculación para un jugador: 8 caracteres, de un solo uso, válido ttlMinutes (1–60, por defecto 15). El jugador pulsa «Vincular cuenta» en el panel del servidor (o usa /link) y lo introduce. Un código nuevo para la misma cuenta anula el anterior.
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:readDe quién es la cuenta: por externalId o por discordId (?discordId=…). Sin vínculo: 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:writeDesvincular una cuenta (o ?discordId=…). Los atributos de la cuenta externa dejan de aplicarse a la persona.
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:writeDar y quitar roles directamente, solo los que el dueño marcó en el portal de desarrolladores → «Claves de API» → «Permitido a las claves» (hasta 10 por lista). remove quita cualquier rol permitido, aunque se haya dado a mano. Lo hace el bot: respuesta 202 con número de tarea.
durationMinutes (1–525 600) — dar los roles de add por un tiempo: al terminar, el bot los quita solo. El plazo nunca acorta uno ya fijado, y un rol que el miembro ya tenía sin plazo sigue siendo permanente. add sin durationMinutes hace el rol permanente, remove cancela la retirada programada.
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:writeEl resultado de una tarea: pending, done o failed, y qué pasó con cada rol.
En una asignación con plazo, result.expiresAt indica cuándo el bot quitará cada rol asignado (null — el miembro ya tenía el rol sin plazo y sigue siendo permanente).
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:writeUn mensaje en nombre del bot (el bot propio del servidor, si está conectado) — solo en los canales que el dueño marcó en el portal de desarrolladores → «Claves de API» → «Permitido a las claves» (hasta 10). content — hasta 2000 caracteres, embeds — hasta 10 embeds simples: title, description, url, color, timestamp, author.name, footer.text, image.url, thumbnail.url, fields; los enlaces solo con https. Por defecto las menciones no avisan a nadie: solo se avisa a los miembros del servidor de mentionUsers (hasta 10), y nunca a @everyone, @here ni a roles. Cuerpo de la solicitud — hasta 32 KB. Lo hace el bot: respuesta 202 con número de tarea.
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:writeEl resultado del envío: pending, done (id del mensaje y enlace) o failed con el motivo: 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..." } }Catálogo
/catalog/servers?search=&tag=&lang=&limit=público · sin claveBuscar y listar servidores en el catálogo de monitoreo. Totalmente público: no se necesita clave.
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>público · sin claveTop del catálogo por Llamas.
curl "https://api.echo-assistants.tech/catalog/top?limit=20"
{ "top": [ { "rank": 1, "slug": "my-server", "name": "My Server", "flames": 980 } ] }/catalog/server/<slug>público · sin claveFicha pública del servidor: descripción, Llamas, puesto, etiquetas, insignias, invitación, valoración.
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>público · sin claveReseñas del servidor con valoración media.
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/tagspúblico · sin claveEtiquetas/categorías del catálogo con el número de servidores.
curl "https://api.echo-assistants.tech/catalog/tags"
{ "tags": [ { "tag": "Игры", "count": 128 } ] }Webhooks
Echo envía eventos a tu URL en tiempo real. Configura un webhook en el panel → «Desarrolladores». Cada solicitud se firma con la cabecera X-Echo-Signature (HMAC-SHA256 del cuerpo con tu secreto); el nombre del evento está en X-Echo-Event.
flame.bumpEl servidor recibió Llama, en el catálogo o con /flame.
{
"event": "flame.bump",
"guildId": "1369...",
"flames": 421,
"flamesTotal": 9101,
"amount": 1,
"source": "web",
"at": "2026-07-27T20:30:00.000Z"
}testSe envía con el botón «Enviar prueba» para comprobar que el receptor funciona.
{ "event": "test", "guildId": "1369...", "message": "Проверка вебхука Echo", "at": "..." }import crypto from "node:crypto";
// body es el texto sin procesar de la petición; secret viene del panel
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);Avisos de integraciones (Premium)
Se configuran en el panel → «Integraciones». Echo envía un POST con JSON {"id", "event", "guildId", "at", "data"}: la firma va en X-Echo-Signature (sha256= más HMAC-SHA256 del cuerpo sin procesar con tu secreto), el nombre del evento en X-Echo-Event y el número de entrega en X-Echo-Delivery.
Responde con 2xx en 8 segundos. Si no, reintentamos a los 30 s, 2 min, 10 min y 1 h con el mismo id; no se siguen redirecciones. La URL debe ser https en 443 con IP pública. Tras 20 fallos seguidos el envío se desactiva: vuelve a activarlo en el panel cuando tu receptor funcione.
member.joinUn miembro entró al servidor.
{ "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.leaveUn miembro salió o fue expulsado.
{ "id": "evt_…", "event": "member.leave", "guildId": "1369...", "at": "…", "data": { "discordId": "123...", "username": "ivan" } }member.level_upUn miembro subió de nivel.
{ "id": "evt_…", "event": "member.level_up", "guildId": "1369...", "at": "…", "data": { "discordId": "123...", "oldLevel": 4, "newLevel": 5, "xp": 1320 } }store.purchaseUna compra en la tienda del servidor: rol asignado, paquete de monedas acreditado (kind "coins", campo coins) o una donación. Las donaciones anónimas no tienen comprador.
{ "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.openedSe abrió un ticket.
{ "id": "evt_…", "event": "ticket.opened", "guildId": "1369...", "at": "…", "data": { "ticketId": 311, "number": 57, "discordId": "123...", "channelId": "1371...", "panelId": 4 } }account.linkedUn jugador vinculó una cuenta con un código.
{ "id": "evt_…", "event": "account.linked", "guildId": "1369...", "at": "…",
"data": { "externalId": "Ivan_Petrov", "discordId": "123...", "username": "ivan", "previousDiscordId": null, "previousExternalId": null } }testEl botón «Enviar prueba» en el panel → «Integraciones».
{ "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 часа. Несколько процессов используют одну таблицу.
// la firma se calcula sobre el cuerpo sin procesar, antes de parsear el 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; }
// la firma se calcula sobre el cuerpo sin procesar, antes de parsear el JSON
$body = file_get_contents('php://input');
if ($body === false || strlen($body) > 65536) { http_response_code(413); exit; }
$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);
$got = $_SERVER['HTTP_X_ECHO_SIGNATURE'] ?? '';
if (!hash_equals($expected, $got)) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
if (!is_array($event) || !is_string($event['id'] ?? null) ||
!preg_match('/^evt_[0-9a-f-]{36}$/D', $event['id']) ||
($event['guildId'] ?? null) !== $guildId ||
!is_string($event['event'] ?? null) || !is_string($event['at'] ?? null) ||
!preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/D', $event['at'])) {
http_response_code(400);
exit;
}
$date = DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.v\Z', $event['at'], new DateTimeZone('UTC'));
$at = $date && $date->format('Y-m-d\TH:i:s.v\Z') === $event['at'] ? $date->getTimestamp() : false;
$age = $at === false ? null : time() - $at;
if ($age === null || $age > 7200 || $age < -300) {
http_response_code(400); exit;
}
// Та же миграция PostgreSQL, что в Node.js-примере.
// ECHO_DATABASE_DSN: pgsql:host=…;dbname=… (PDO_PGSQL).
$db = new PDO(getenv('ECHO_DATABASE_DSN'), getenv('ECHO_DATABASE_USER'), getenv('ECHO_DATABASE_PASSWORD'),
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
try {
$db->beginTransaction();
$claim = $db->prepare('INSERT INTO echo_webhook_events (guild_id, event_id) VALUES (?, ?) ON CONFLICT DO NOTHING RETURNING event_id');
$claim->execute([$event['guildId'], $event['id']]);
if ($claim->fetchColumn() === false) {
$db->rollBack(); http_response_code(204); exit;
}
// Изменение награды/баланса делайте через $db В ЭТОЙ транзакции.
// Внешние действия — через outbox, записанный здесь.
$db->commit();
http_response_code(204);
} catch (Throwable $e) {
if ($db->inTransaction()) $db->rollBack();
http_response_code(500);
}Scopes
Al crear una clave eliges permisos: cada endpoint requiere su propio scope.
guild:readeconomy:readeconomy:writelevels:readmonitoring:readmembers:readmembers:writeroles:writelinks:readlinks:writelevels:writemessages:writeLímites y errores
Hasta 3 (10 con Premium) claves por servidor, con un límite de 40 (120 con Premium) solicitudes por minuto por clave. Los errores se devuelven como JSON { "error": "..." } con el código HTTP correspondiente.
| Código | error | Cuándo |
|---|---|---|
| 400 | bad_fields | Parámetros de la solicitud inválidos o ausentes. |
| 401 | unauthorized | Clave no proporcionada, inválida o revocada. |
| 403 | insufficient_scope | La clave carece del scope necesario para esta solicitud. |
| 403 | key_paused_premium | La clave está en pausa: el servidor no tiene Premium y sin él solo funcionan las claves dentro del límite gratuito que se emitieron primero. |
| 404 | not_linked | La cuenta no está vinculada (métodos de vínculos) o no se encontró la tarea. |
| 409 | levels_off | La acción no encaja con la configuración del servidor: por ejemplo, los niveles están desactivados. |
| 413 | body_too_large | El cuerpo de la petición supera los 8 KB. |
| 429 | rate_limited | Se superó el límite de 40 (120 con Premium) solicitudes por minuto. |
| 502 | discord_unavailable | Discord no respondió; repita la solicitud más tarde. |
La clave se muestra completa solo una vez al crearla: guárdala. La administración del proyecto puede desactivar una clave en caso de abuso.