API Echo
Herkese açık REST API ve webhook’lar: sunucunun ekonomisini, seviyelerini ve sunucu listesi verilerini kendi hizmetlerine bağla — bir oyun sunucusuna, siteye, bota veya otomasyonlara.
https://api.echo-assistants.tech
Yetkilendirme
Her istek, Authorization başlığında bir anahtar gerektirir. Anahtar bir hesaba değil, sunucuya verilir. Anahtarı panelde → “Geliştiriciler” bölümünden yalnızca sunucu sahibi oluşturabilir: en fazla 3 (Premium ile 10) anahtar. /catalog/* uç noktaları herkese açıktır — anahtar gerekmez (IP başına 60/dk sınırı, yanıtlar önbelleğe alınır).
curl https://api.echo-assistants.tech/me \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Anahtar güvenliği
Anahtar echoapi_ ile başlar, ardından 30 rastgele Latin harfi ve rakam, en sonda da 6 karakterlik sağlama toplamı gelir. Sağlama toplamı yanlış olan bir dizeyi API hemen reddeder (401). Eski biçimdeki anahtarlar (echo_…) çalışmaya devam eder; yeniden oluşturulan anahtar yeni biçimi alır.
\bechoapi_[0-9A-Za-z]{36}\bHerkese açık bir yerde görünen anahtar otomatik olarak devre dışı bırakılır. GitHub, Secret Scanning programı kapsamında herkese açık depolarda bulunan anahtarları bize bildirir; Echo botu da sunuculardaki mesajlarda anahtarları fark edip bu mesajları siler. Sunucu sahibi ve anahtarı oluşturan kişi sitede, Telegram’da ve e-postayla bildirim alır; bildirimde anahtarın bulunduğu yerin bağlantısı olur.
Ne yapmalı: geliştirici portalında sunucunun anahtarlarını aç ve anahtar kartında Yeniden oluştur butonuna bas. Anahtar yeni bir gizli değer alır ve yeniden çalışır; eskisi hemen çalışmaz hale gelir. Anahtarı kayıtlı olduğu her yerde değiştir. Bulunan bir anahtar yeniden oluşturulmadan etkinleştirilemez. Anahtara artık ihtiyacın yoksa iptal et.
Anahtarı kendi sunucunda tut — ortam değişkenlerinde veya bir gizli bilgi deposunda. Sayfa ya da uygulama koduna, depolara, mesajlara veya ekran görüntülerine koyma.
Hizmet
/mekapsamsızAnahtar kontrolü: sunucuyu ve kapsamlarını döndürür.
curl "https://api.echo-assistants.tech/me" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "guildId": "1369...", "scopes": ["levels:read"] }Ekonomi
/economy/balance?user=<id>economy:readÜye bakiyesi: cüzdan, banka, fişler.
curl "https://api.echo-assistants.tech/economy/balance?user=123456789012345678" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "user": "123...", "balance": 1500, "bank": 200, "chips": 0 }/economy/balanceeconomy:writePara ekle veya düş. delta negatif olabilir; bakiye 0…2.000.000.000 aralığında kalır.
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:readBakiyeye göre en iyi üyeler (limit 1–100, varsayılan 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 } ] }Seviyeler
/levels/leaderboard?limit=<n>levels:readDeneyime göre en iyi üyeler (limit 1–100, varsayılan 20).
curl "https://api.echo-assistants.tech/levels/leaderboard?limit=20" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "guildId": "1369...", "leaderboard": [ { "rank": 1, "userId": "627...", "xp": 3965, "messages": 374, "voiceSeconds": 1273 } ] }/levels/xp?user=<id>levels:readÜyenin XP’si ve seviyesi; nextLevelXp, bir sonraki seviye için gereken toplam XP’dir.
curl "https://api.echo-assistants.tech/levels/xp?user=123456789012345678" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "user": "123...", "xp": 1320, "level": 4, "nextLevelXp": 1500 }/levels/xplevels:writeXP ekle veya düş: delta bir tam sayıdır, istek başına en fazla ±1.000.000; XP sıfırın altına inmez. Seviye hemen yeniden hesaplanır. Seviye yükselirse bot, normal seviye atlamada olduğu gibi seviye ödüllerini (roller, coin, duyuru) verir ve "via": "api" işaretli member.level_up olayını gönderir. Seviye düşünce ödüller geri alınmaz. Yalnızca sunucu üyeleri için (aksi halde 404 not_member); sunucuda seviyeler kapalıysa — 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 }Sunucu listesi
/monitoring/servermonitoring:readKatalogdaki sunucu kartı: dönemdeki Alevler, toplam, sıralamadaki yeri ve durumlar.
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:readAlevlere göre katalogdaki en iyi sunucular (limit 1–100, varsayılan 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 } ] }Entegrasyonlar
Oyununu, siteni veya CRM’ini bağla: üye öznitelikleri panelde → “Entegrasyonlar” bölümündeki kurallara göre rollere dönüşür, oyuncular hesaplarını kodla bağlar ve izinli roller doğrudan verilebilir. Yazma istek gövdeleri en fazla 8 KB JSON olabilir.
/members/<discordId>/attributesmembers:writeÜyenin özniteliklerini gönder (tamamen değiştirir): en fazla 25 alan, adlar Latin harfleri/rakamlar/_, değerler 100 karaktere kadar metin, sayı veya true/false. Bot, kurallara bağlı rolleri hemen yeniden hesaplar.
curl -X PUT "https://api.echo-assistants.tech/members/123456789012345678/attributes" \
-H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{ "attributes": { "faction": "police", "rank": 3, "vip": true } }'{ "attributes": { "faction": "police", "rank": 3, "vip": true } }{ "ok": true, "discordId": "123...", "attributes": { "faction": "police", "rank": 3, "vip": true } }/members/<discordId>/attributesmembers:readÜye için ne gönderildiği ve hangi harici hesabı bağladığı.
timedRoles — API için izinli rollerden süreli verilenler ve botun bunları ne zaman kaldıracağı.
curl "https://api.echo-assistants.tech/members/123456789012345678/attributes" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "discordId": "123...", "externalId": "Ivan_Petrov", "attributes": { "faction": "police", "rank": 3 }, "timedRoles": [ { "roleId": "1369...", "expiresAt": "2026-10-10T12:00:00.000Z" } ] }/members/<discordId>/attributesmembers:writeÜyenin tüm özniteliklerini temizle. Bot, kuralların verdiği rolleri kaldırır; elle verilen rollere dokunmaz.
curl -X DELETE "https://api.echo-assistants.tech/members/123456789012345678/attributes" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "ok": true, "removed": true }/external/<externalId>/attributesmembers:writeAynısı, senin sistemindeki ID ile (takma ad, UUID, license:…). Hesabı kodla bağlayan kişiye uygulanır; bağlanana kadar saklanır ve bağlandığında uygulanır. Ad çakışmalarında Discord ID ile gönderilen öznitelikler önceliklidir.
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:readHarici hesap için ne gönderildiği ve onu kimin bağladığı (discordId: null — henüz kimse).
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:writeHarici bir hesabın tüm özniteliklerini temizle.
curl -X DELETE "https://api.echo-assistants.tech/external/Ivan_Petrov/attributes" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "ok": true, "removed": true }/link-codeslinks:writeOyuncu için bağlantı kodu: 8 karakter, tek kullanımlık, ttlMinutes süresince geçerli (1–60, varsayılan 15). Oyuncu sunucu panelinde “Hesabı bağla” butonuna basar (veya /link kullanır) ve kodu girer. Aynı hesap için yeni kod, öncekini geçersiz kılar.
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:readKimin hesabı: externalId veya discordId ile (?discordId=…). Bağlantı yoksa — 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:writeHesap bağlantısını kaldır (veya ?discordId=…). Harici hesabın öznitelikleri artık o kişiye uygulanmaz.
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:writeRolleri doğrudan ver ve kaldır — yalnızca sahibin geliştirici portalında → “API anahtarları” → “Anahtarlara izin verilenler” bölümünde işaretlediği roller (her listede en fazla 10). remove, elle verilmiş olsa bile izinli herhangi bir rolü kaldırır. İşlemi bot yapar: yanıt olarak görev numarasıyla 202 döner.
durationMinutes (1–525.600) — add içindeki rolleri belirli bir süre için verir: süre dolunca bot rolleri kendisi kaldırır. Süre, önceden belirlenmiş bir süreyi kısaltmaz; üyede zaten süresiz olan rol kalıcı kalır. durationMinutes olmadan add rolü kalıcı yapar, remove planlanmış kaldırmayı iptal eder.
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:writeRol verme görevinin sonucu: pending, done veya failed ve her rolde ne olduğu.
Süreli rol vermede result.expiresAt, botun verilen her rolü ne zaman kaldıracağını gösterir (null — rol üyede zaten süresiz vardı ve kalıcı kalır).
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:writeBot adına mesaj (bağlıysa sunucunun kendi botu) — yalnızca sahibin geliştirici portalında → “API anahtarları” → “Anahtarlara izin verilenler” bölümünde işaretlediği kanallara (en fazla 10). content — en fazla 2000 karakter, embeds — en fazla 10 basit embed: title, description, url, color, timestamp, author.name, footer.text, image.url, thumbnail.url, fields; bağlantılar yalnızca https. Bahsetmeler varsayılan olarak kimseyi çağırmaz: yalnızca mentionUsers listesindeki sunucu üyeleri (en fazla 10) çağrılabilir, @everyone, @here ve roller asla. İstek gövdesi — en fazla 32 KB. İşlemi bot yapar: yanıt olarak görev numarasıyla 202 döner.
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:writeGönderimin sonucu: pending, done (mesaj id’si ve bağlantısı) veya nedeniyle birlikte failed: channel_not_allowed, channel_missing, missing_permissions, rejected.
curl "https://api.echo-assistants.tech/message-ops/42" \ -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
{ "opId": 77, "channelId": "1371...", "status": "done", "result": { "messageId": "1372...", "channelId": "1371...", "url": "https://discord.com/channels/1369.../1371.../1372..." } }Katalog
/catalog/servers?search=&tag=&lang=&limit=herkese açık · anahtarsızSunucu listesi kataloğunda arama ve sunucu listeleme. Tamamen herkese açık — anahtar gerekmez.
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>herkese açık · anahtarsızAlevlere göre katalog sıralaması.
curl "https://api.echo-assistants.tech/catalog/top?limit=20"
{ "top": [ { "rank": 1, "slug": "my-server", "name": "My Server", "flames": 980 } ] }/catalog/server/<slug>herkese açık · anahtarsızHerkese açık sunucu kartı: açıklama, Alevler, sıra, etiketler, rozetler, davet, puan.
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>herkese açık · anahtarsızOrtalama puanla birlikte sunucu yorumları.
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/tagsherkese açık · anahtarsızSunucu sayılarıyla birlikte katalog etiketleri/kategorileri.
curl "https://api.echo-assistants.tech/catalog/tags"
{ "tags": [ { "tag": "Игры", "count": 128 } ] }Webhook’lar
Echo, olayları gerçek zamanlı olarak belirttiğin URL’ye gönderir. Webhook’u panelde → “Geliştiriciler” bölümünden kur. Her istek X-Echo-Signature başlığıyla imzalanır (gövdeden gizli anahtarınla hesaplanan HMAC-SHA256); olay adı X-Echo-Event içinde yer alır.
flame.bumpSunucuya katalogda ya da /flame ile Alev verildi.
{
"event": "flame.bump",
"guildId": "1369...",
"flames": 421,
"flamesTotal": 9101,
"amount": 1,
"source": "web",
"at": "2026-07-27T20:30:00.000Z"
}test“Test gönder” butonuyla gönderilir — alıcının çalıştığını kontrol etmek için.
{ "event": "test", "guildId": "1369...", "message": "Проверка вебхука Echo", "at": "..." }import crypto from "node:crypto";
// body ham istek metnidir, secret panelden alınır
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);Entegrasyon bildirimleri (Premium)
Panelde → “Entegrasyonlar” bölümünden ayarlanır. Echo JSON ile POST gönderir {"id", "event", "guildId", "at", "data"}: imza — X-Echo-Signature (sha256= ve ham gövdeden gizli anahtarınla hesaplanan HMAC-SHA256), olay adı — X-Echo-Event, teslimat numarası — X-Echo-Delivery.
8 saniye içinde 2xx ile yanıt ver. Aksi halde aynı id ile 30 sn, 2 dk, 10 dk ve 1 sa sonra yeniden deneriz; yönlendirmeler izlenmez. Adres yalnızca 443 portunda ve herkese açık IP’de https olmalıdır. Art arda 20 başarısızlıktan sonra gönderim kapanır — alıcın çalışır hale gelince panelden yeniden aç.
member.joinBir üye sunucuya katıldı.
{ "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.leaveBir üye ayrıldı veya atıldı.
{ "id": "evt_…", "event": "member.leave", "guildId": "1369...", "at": "…", "data": { "discordId": "123...", "username": "ivan" } }member.level_upBir üye seviye atladı.
{ "id": "evt_…", "event": "member.level_up", "guildId": "1369...", "at": "…", "data": { "discordId": "123...", "oldLevel": 4, "newLevel": 5, "xp": 1320 } }store.purchaseSunucu vitrininde bir satın alma: rol verildi, coin paketi yüklendi (kind "coins", coins alanı) veya sunucuya destek. Anonim desteklerde alıcı yoktur.
{ "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.openedBir destek talebi açıldı.
{ "id": "evt_…", "event": "ticket.opened", "guildId": "1369...", "at": "…", "data": { "ticketId": 311, "number": 57, "discordId": "123...", "channelId": "1371...", "panelId": 4 } }account.linkedBir oyuncu hesabını kodla bağladı.
{ "id": "evt_…", "event": "account.linked", "guildId": "1369...", "at": "…",
"data": { "externalId": "Ivan_Petrov", "discordId": "123...", "username": "ivan", "previousDiscordId": null, "previousExternalId": null } }testPanelde → “Entegrasyonlar” bölümündeki “Test gönder” butonu.
{ "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 часа. Несколько процессов используют одну таблицу.
// imza, JSON ayrıştırılmadan önce ham gövde üzerinden hesaplanır
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; }
// imza, JSON ayrıştırılmadan önce ham gövde üzerinden hesaplanır
$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);
}Kapsamlar
Anahtar oluştururken izinleri seçersin — her uç nokta kendi kapsamını gerektirir.
guild:readeconomy:readeconomy:writelevels:readmonitoring:readmembers:readmembers:writeroles:writelinks:readlinks:writelevels:writemessages:writeSınırlar ve hatalar
Sunucu başına en fazla 3 (Premium ile 10) anahtar; her anahtar için dakikada 40 (Premium ile 120) istek sınırı. Hatalar, ilgili HTTP koduyla birlikte JSON { "error": "..." } olarak döner.
| Kod | error | Ne zaman |
|---|---|---|
| 400 | bad_fields | Geçersiz veya eksik istek parametreleri. |
| 401 | unauthorized | Anahtar gönderilmedi, geçersiz veya iptal edilmiş. |
| 403 | insufficient_scope | Anahtarda bu istek için gereken kapsam yok. |
| 403 | key_paused_premium | Anahtar duraklatıldı: sunucuda Premium yok ve Premium olmadan yalnızca ücretsiz sınır içinde ilk oluşturulan anahtarlar çalışır. |
| 404 | not_linked | Hesap bağlı değil (bağlantı yöntemleri) veya görev bulunamadı. |
| 409 | levels_off | İşlem sunucu ayarlarına uymuyor: örneğin seviyeler kapalı. |
| 413 | body_too_large | İstek gövdesi 8 KB sınırını aşıyor. |
| 429 | rate_limited | Dakikada 40 (Premium ile 120) istek sınırı aşıldı. |
| 502 | discord_unavailable | Discord yanıt vermedi — isteği daha sonra tekrarla. |
Anahtar yalnızca oluşturulduğunda bir kez tam olarak gösterilir — onu kaydet. Proje yönetimi, kötüye kullanım durumunda bir anahtarı devre dışı bırakabilir.