API Echo

Ашық REST API және вебхуктар: сервердің экономикасын, деңгейлерін және мониторингін өз сервистеріңе — ойын серверіне, сайтқа, ботқа немесе автоматтандыруға қос.

https://api.echo-assistants.tech

Авторизация

Әр сұрауға Authorization тақырыбында кілт керек. Кілт аккаунтқа емес, серверге беріледі. Оны басқару панелі → «Әзірлеушілерге» бөлімінде тек сервер иесі жасай алады: 3 (Premium-мен — 10) кілтке дейін. /catalog/* эндпоинттері ашық, кілт керек емес (IP бойынша минутына 60 шегі, жауаптар кэштеледі).

curl https://api.echo-assistants.tech/me \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Кілт жасау

Кілттер қауіпсіздігі

Кілт echoapi_ арқылы басталады, одан кейін 30 кездейсоқ латын әрпі мен цифр және бақылау сомасының 6 таңбасы келеді. Сомасы қате жолды API бірден қабылдамайды (401). Бұрынғы түрдегі кілттер (echo_…) жұмыс істей береді; қайта шығарғанда кілт жаңа түрге ауысады.

Өз кодыңыздан кілттерді іздеуге арналған өрнек
\bechoapi_[0-9A-Za-z]{36}\b

Жария болған кілт автоматты түрде өшіріледі. GitHub Secret Scanning бағдарламасы арқылы бізге жария репозиторийлердегі кілттер туралы хабарлайды, ал Echo боты серверлердегі хабарламаларда кілттерді байқап, мұндай хабарламаларды жояды. Сервер иесі мен кілтті жасаған адам сайтта, Telegram-да және поштада хабарландыру алады — табылған жердің сілтемесімен.

Не істеу керек: әзірлеуші кабинетінде сервер кілттерін ашып, кілт карточкасындағы Қайта шығару түймесін басыңыз. Кілт жаңа құпия алып, қайта жұмыс істей бастайды, бұрынғысы бірден әрекет етуін тоқтатады. Кілтті жазылған барлық жерде ауыстырыңыз. Табылған кілтті қайта шығармай қосуға болмайды. Кілт енді керек болмаса, оны кері алыңыз.

Кілтті өз серверіңізде — орта айнымалыларында немесе құпиялар қоймасында сақтаңыз. Оны беттер мен қолданбалардың кодына, репозиторийлерге, хабарламаларға және скриншоттарға қоймаңыз.

Сервис

GET/meскоупсыз

Кілтті тексеру: серверді және оның скоуптарын қайтарады.

Мысал
curl "https://api.echo-assistants.tech/me" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Жауап
{ "guildId": "1369...", "scopes": ["levels:read"] }

Экономика

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

Қатысушы балансы: әмиян, банк, фишкалар.

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

Валюта қосу немесе шегеру. delta теріс болуы мүмкін; баланс 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 }
GET/economy/leaderboard?limit=<n>economy:read

Баланс бойынша үздік қатысушылар (limit 1–100, әдепкі 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 } ] }

Деңгейлер

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

Тәжірибе бойынша үздік қатысушылар (limit 1–100, әдепкі 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 } ] }
GET/levels/xp?user=<id>levels:read

Қатысушының тәжірибесі мен деңгейі; nextLevelXp — келесі деңгейге барлығы қанша тәжірибе керек.

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

Тәжірибе қосу немесе шегеру: delta — бүтін сан, бір сұрауға ±1 000 000 дейін; тәжірибе нөлден төмен түспейді. Деңгей бірден қайта есептеледі. Егер ол өссе, бот әдеттегі деңгей көтерудегідей деңгей сыйлықтарын (рөлдер, монеталар, жарияланым) береді және "via": "api" белгісімен member.level_up оқиғасын жібереді. Деңгей төмендегенде сыйлықтар алынбайды. Тек сервер қатысушылары үшін (әйтпесе 404 not_member); серверде деңгейлер өшірулі болса — 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 }

Мониторинг

GET/monitoring/servermonitoring:read

Каталогтағы сервер карточкасы: кезеңдегі Жалын, барлығы, үздіктер тізіміндегі орны және күйлері.

Мысал
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 }
GET/monitoring/top?limit=<n>monitoring:read

Жалын бойынша каталогтың үздік серверлері (limit 1–100, әдепкі 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 } ] }

Интеграциялар

Өз ойыныңды, сайтыңды немесе CRM-ді қосу: қатысушылардың атрибуттары басқару панелі → «Интеграциялар» бөліміндегі ережелер бойынша рөлдерге айналады, ойыншылар аккаунттарын кодпен байланыстырады, рұқсат етілген рөлдерді тікелей беруге болады. Жазба денесі — 8 КБ-қа дейінгі JSON.

PUT/members/<discordId>/attributesmembers:write

Қатысушы атрибуттарын беру (толығымен ауыстыру): 25 өріске дейін, атауы — латын әріптері/цифрлар/_, мәні — 100 таңбаға дейінгі жол, сан немесе true/false. Ережелер бойынша рөлдерді бот бірден қайта есептейді.

Мысал
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 } }
GET/members/<discordId>/attributesmembers:read

Қатысушы бойынша не жіберілгені және ол қай сыртқы аккаунтты байланыстырғаны.

timedRoles — API үшін рұқсат етілгендер ішінен мерзімге берілген рөлдер және бот оларды қашан алатыны.

Мысал
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" } ] }
DELETE/members/<discordId>/attributesmembers:write

Қатысушының барлық атрибуттарын алып тастау. Ереже берген рөлдерді бот алады; қолмен берілгендеріне тиіспейді.

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

Дәл солай, бірақ жүйеңдегі ID бойынша (ник, UUID, license:…). Аккаунтты кодпен байланыстырған адамға әсер етеді; әзірге байланыстырылмаса — сақталады да, байланыстырғанда қолданылады. Атауы сәйкес келсе, Discord ID бойынша атрибуттардың басымдығы жоғары.

Мысал
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 } }
GET/external/<externalId>/attributesmembers:read

Сыртқы аккаунт бойынша не жіберілгені және оны кім байланыстырғаны (discordId: null — әзірге ешкім).

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

Сыртқы аккаунттың барлық атрибуттарын алып тастау.

Мысал
curl -X DELETE "https://api.echo-assistants.tech/external/Ivan_Petrov/attributes" \
  -H "Authorization: Bearer echoapi_ВАШ_КЛЮЧ"
Жауап
{ "ok": true, "removed": true }
POST/link-codeslinks:write

Ойыншыға байланыстыру коды: 8 таңба, бір реттік, ttlMinutes бойы жарамды (1–60, әдепкі 15). Ойыншы сервер панеліндегі «Аккаунтты байланыстыру» түймесін басады (немесе /link) да, кодты енгізеді. Сол аккаунттың жаңа коды алдыңғысын жояды.

Мысал
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" }
GET/links?externalId=<externalId>links:read

Кімнің аккаунты: externalId немесе discordId бойынша (?discordId=…). Байланыс жоқ болса — 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" }
DELETE/links?externalId=<externalId>links:write

Аккаунтты ажырату (немесе ?discordId=…). Сыртқы аккаунттың атрибуттары адамға әсер етуін тоқтатады.

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

Рөлдерді тікелей беру және алу — тек ие әзірлеуші кабинетінде → «API кілттері» → «Кілттерге рұқсат етілген» бөлімінде белгілегендерін (әр тізімде 10-ға дейін). remove рұқсат етілген кез келген рөлді, тіпті қолмен берілгенін де алады. Ботпен орындалады: тапсырма нөмірімен 202 жауабы.

durationMinutes (1–525 600) — add ішіндегі рөлдерді мерзімге беру: мерзімі біткенде бот оларды өзі алып тастайды. Мерзім бұрын тағайындалғанын қысқартпайды, ал қатысушыда мерзімсіз болған рөл мерзімсіз күйінде қалады. durationMinutes жоқ add рөлді мерзімсіз етеді, remove тағайындалған алып тастауды болдырмайды.

Мысал
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" }
GET/role-ops/<opId>roles:write

Рөл беру тапсырмасының нәтижесі: pending, done немесе failed және әр рөл бойынша не шыққаны.

Мерзімге берілгенде result.expiresAt ішінде бот әр берілген рөлді қашан алатыны көрсетіледі (null — рөл қатысушыда мерзімсіз болған және мерзімсіз қалды).

Мысал
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" } } }
POST/channels/<channelId>/messagesmessages:write

Бот атынан хабарлама (қосылған болса, сервердің өз боты) — тек ие әзірлеуші кабинетінде → «API кілттері» → «Кілттерге рұқсат етілген» бөлімінде белгілеген арналарға (10-ға дейін). content — 2000 таңбаға дейін, embeds — 10 қарапайым эмбедке дейін: title, description, url, color, timestamp, author.name, footer.text, image.url, thumbnail.url, fields; сілтемелер тек https. Әдепкі бойынша аталымдар ешкімді шақырмайды: тек mentionUsers тізіміндегі сервер қатысушыларын (10-ға дейін) шақыруға болады, ал @everyone, @here және рөлдер ешқашан шақырылмайды. Сұрау денесі — 32 КБ-қа дейін. Ботпен орындалады: тапсырма нөмірімен 202 жауабы.

Мысал
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" }
GET/message-ops/<opId>messages:write

Жіберу нәтижесі: pending, done (хабарлама id-і және сілтеме) немесе себебі көрсетілген 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..." } }

Каталог

GET/catalog/servers?search=&tag=&lang=&limit=ашық · кілтсіз

Мониторинг каталогындағы серверлерді іздеу және тізімі. Барлығы ашық — кілт керек емес.

Мысал
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 } } ] }
GET/catalog/top?limit=<n>ашық · кілтсіз

Жалын бойынша каталог үздіктері.

Мысал
curl "https://api.echo-assistants.tech/catalog/top?limit=20"
Жауап
{ "top": [ { "rank": 1, "slug": "my-server", "name": "My Server", "flames": 980 } ] }
GET/catalog/server/<slug>ашық · кілтсіз

Сервердің ашық карточкасы: сипаттама, Жалын, орны, тегтер, белгілер, шақыру, рейтинг.

Мысал
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/..." }
GET/catalog/reviews/<slug>?limit=<n>ашық · кілтсіз

Сервер туралы пікірлер және орташа баға.

Мысал
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": "..." } ] }
GET/catalog/tagsашық · кілтсіз

Каталог тегтері/санаттары және ондағы серверлер саны.

Мысал
curl "https://api.echo-assistants.tech/catalog/tags"
Жауап
{ "tags": [ { "tag": "Игры", "count": 128 } ] }

Вебхуктар

Echo оқиғаларды URL-іңе нақты уақытта өзі жібереді. Вебхукты басқару панелі → «Әзірлеушілерге» бөлімінде бапта. Әр сұрау X-Echo-Signature тақырыбымен қол қойылған (құпия кілтің бойынша дененің HMAC-SHA256 мәні), оқиға атауы — X-Echo-Event ішінде.

flame.bump

Серверге каталогта немесе /flame командасымен Жалын берілді.

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

«Тест жіберу» түймесімен жіберіледі — қабылдағыштың жұмыс істейтінін тексеру үшін.

{ "event": "test", "guildId": "1369...", "message": "Проверка вебхука Echo", "at": "..." }
Қолтаңбаны тексеру
import crypto from "node:crypto";

// body — сұраудың өңделмеген мәтіні, secret — басқару панелінен
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);

Интеграция хабарландырулары (Premium)

Басқару панелі → «Интеграциялар» бөлімінде бапталады. Echo JSON-мен POST жібереді {"id", "event", "guildId", "at", "data"}: қолтаңба — X-Echo-Signature (sha256= және құпияң бойынша шикі дененің HMAC-SHA256-сы), оқиға атауы — X-Echo-Event, жеткізу нөмірі — X-Echo-Delivery.

8 секунд ішінде 2xx жауап бер. Әйтпесе — сол id-мен 30 с, 2 мин, 10 мин және 1 сағаттан кейін қайталау; қайта бағыттаулар орындалмайды. Мекенжай — тек 443 портындағы https және жария IP. Қатарынан 20 сәтсіздіктен кейін жіберу өшіріледі — қабылдағыш жұмыс істей бастағанда, оны басқару панелінде қос.

member.join

Қатысушы серверге қосылды.

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

Қатысушы шықты немесе оны шығарып жіберді.

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

Қатысушы деңгейін көтерді.

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

Сервер витринасында сатып алу: рөл берілді, монета пакеті есептелді (kind "coins", coins өрісі) немесе серверге қолдау. Анонимді қолдаудың сатып алушысы жоқ.

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

Тикет ашылды.

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

Ойыншы аккаунтты кодпен байланыстырды.

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

Басқару панелі → «Интеграциялар» бөліміндегі «Тест жіберу» түймесі.

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

// қолтаңба 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
<?php
$secret = getenv('ECHO_WEBHOOK_SECRET'); // whsec_… из панели
$guildId = getenv('ECHO_GUILD_ID');
if (!$secret || !$guildId) { http_response_code(500); exit; }
// қолтаңба 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);
}

Скоуптар

Кілт жасағанда құқықтар таңдалады — әр эндпоинт өз скоупын талап етеді.

guild:read
Сервер деректері
Сервер туралы негізгі ақпарат.
economy:read
Экономиканы оқу
Баланстар: әмиян, банк, фишкалар.
economy:write
Экономикаға жазу
Валюта қосу және шегеру.
levels:read
Деңгейлерді оқу
Қатысушылардың тәжірибесі мен үздіктер тізімі.
monitoring:read
Мониторингті оқу
Жалын, үздіктер тізіміндегі орын, каталогтағы күйлер.
members:read
Атрибуттарды оқу
Қатысушылардың атрибуттарын Discord ID және сыртқы жүйедегі ID бойынша оқу — оларды өзгерту құқығынсыз.
members:write
Қатысушылар атрибуттары
Атрибуттарды Discord ID және сыртқы жүйедегі ID бойынша беру.
roles:write
Рөл беру
Әзірлеуші кабинетінде → «API кілттері» → «Кілттерге рұқсат етілген» бөлімінде рұқсат етілген рөлдерді беру және алу.
links:read
Байланыстарды оқу
Кімнің аккаунты: сыртқы ID мен Discord байланысы.
links:write
Байланыстыру кодтары
Байланыстыру кодтарын шығару және аккаунттарды ажырату.
levels:write
Тәжірибені жазу
Тәжірибе қосу және шегеру; деңгей сыйлықтарын бот береді.
messages:write
Боттан хабарламалар
Әзірлеуші кабинетінде → «API кілттері» → «Кілттерге рұқсат етілген» бөлімінде рұқсат етілген арналарға бот атынан хабарламалар.

Шектер мен қателер

Бір серверге 3 (Premium-мен — 10) кілтке дейін, шек — әр кілтке минутына 40 (Premium-мен — 120) сұрау. Қателер тиісті HTTP-кодпен JSON { "error": "..." } түрінде келеді.

КодerrorҚашан
400bad_fieldsСұрау параметрлері қате немесе жетіспейді.
401unauthorizedКілт берілмеген, қате немесе кері қайтарылған.
403insufficient_scopeКілтте бұл сұрауға қажетті скоуп жоқ.
403key_paused_premiumКілт кідіртілген: серверде Premium жоқ, ал онсыз тек тегін шек ішіндегі, басқалардан бұрын шығарылған кілттер жұмыс істейді.
404not_linkedАккаунт байланыстырылмаған (байланыс әдістері) немесе тапсырма табылмады.
409levels_offӘрекет сервер баптауларына сай келмейді: мысалы, деңгейлер өшірулі.
413body_too_largeСұрау денесі 8 КБ-тан үлкен.
429rate_limitedМинутына 40 (Premium-мен — 120) сұрау шегінен асты.
502discord_unavailableDiscord жауап бермеді — сұрауды кейінірек қайталаңыз.

Кілт толық күйінде тек жасалған кезде бір рет көрінеді — оны сақтап қой. Теріс пайдаланылса, жоба әкімшілігі кілтті өшіре алады.

Өзгерістерді сақтауды ұмытпаңыз.
Сақталмаған өзгерістер бар — қолданасың ба, әлде болдырмайсың ба?