Игра открывается внутри платформы с уже подписанной личностью игрока — ни регистрации, ни своего логина не нужно. Отсюда доступны профиль, друзья, публикация результата и платежи.
Нужен мультиплеер, чат, сохранения, рекорды или игровая экономика — они тоже готовы: Zloy Backend, свой сервер писать не придётся.
Платформа подписывает игрока и передаёт его игре в адресе страницы.
Игра открывается на странице zloy.net/<адрес_игры>
внутри iframe. К адресу платформа добавляет четыре параметра —
подписанную личность игрока:
app_id | идентификатор вашей игры |
user_id | идентификатор игрока, открывшего игру |
ts | unix-время выдачи подписи (действует 48 часов) |
sign | HMAC-SHA256(app_key, "app_id:user_id:ts") в hex |
Эти параметры передаются в каждый запрос без изменений. Подделать
подпись без app_key нельзя; ключ виден только вам на
странице редактирования игры, там же его можно отозвать — старые
подписи мгновенно перестают действовать.
# Адрес, по которому открылась игра:
# https://ваш-адрес/?app_id=1&user_id=42&ts=1760000000&sign=<hex>
# Godot Web: строку параметров берём из адреса страницы
var query := JavaScriptBridge.eval("location.search.slice(1)", true)
func api_url(method: String) -> String:
return "https://api.zloy.net/v1/%s?%s" % [method, query]
// Адрес, по которому открылась игра:
// https://ваш-адрес/?app_id=1&user_id=42&ts=1760000000&sign=<hex>
// SDK подхватывает параметры сам — руками их читать не нужно
const p = new URLSearchParams(location.search);
console.log('игрок', p.get('user_id'));
interface Identity {
app_id: string; user_id: string; ts: string; sign: string;
}
const p = new URLSearchParams(location.search);
const identity: Identity = {
app_id: p.get('app_id') ?? '',
user_id: p.get('user_id') ?? '',
ts: p.get('ts') ?? '',
sign: p.get('sign') ?? '',
};
Отдельное приложение авторизуется само — через системный браузер, без пароля внутри игры.
Когда игра собрана отдельным приложением — .exe,
сборка под Linux, APK для RuStore, Huawei, Xiaomi или Samsung —
адреса страницы с подписью нет: подписать личность игрока некому.
Ключ игры app_key в сборку класть нельзя — его
достанут из любого пакета.
Поэтому вход идёт по схеме OAuth 2.0 Authorization Code + PKCE (RFC 7636 и 8252): игра открывает zloy.net в системном браузере, игрок подтверждает вход на нашем домене, браузер возвращает в игру одноразовый код, и уже код меняется на токены. Пароль игра не видит никогда, а перехваченный код бесполезен без секрета, который остался у игры в памяти.
Всё это делает SDK: вам нужен один вызов
await Zloy.login(). Он же сам определит, что игра
запущена внутри платформы, и тогда не покажет игроку ничего.
| Возврат в игру | http://127.0.0.1:<любой порт>/… — работает на всех платформах и ничего не требует от сборки: ни intent-filter, ни схемы в реестре Windows. Дополнительно разрешена схема zloy.<адрес игры>://… |
| Если браузер не вернулся | await Zloy.login_by_code(): игра показывает шесть символов, игрок вводит их на zloy.net/link |
| Повторные запуски | вход не требуется: SDK хранит долгий токен в user:// и обновляет доступ сам |
| Отзыв доступа | игрок в любой момент выходит из игры в разделе Вход и безопасность |
| Один аккаунт — много игр | сессия живёт в системном браузере, поэтому во вторую игру Zloy игрок входит одним нажатием |
app_id и адрес игры платформа кладёт в пакет сама
(файл zloy_client.json, появляется после первой
публикации) — добавлять его в проект не нужно.
# zloy_sdk.gd подключён автозагрузкой под именем «Zloy»
func _ready() -> void:
if await Zloy.login():
print("привет, ", Zloy.user.nickname)
# кубики авторизуются тем же входом:
var ws := WebSocketPeer.new()
ws.connect_to_url(await Zloy.cubes_url())
else:
# игрок отказался — играем без аккаунта
pass
# запасной вход, если браузер не смог вернуться в игру
func sign_in_by_code() -> void:
Zloy.device_code_ready.connect(
func(code, uri): $Label.text = "Введите %s на %s" % [code, uri])
await Zloy.login_by_code()
# выход на этом устройстве
func sign_out() -> void:
await Zloy.logout()
// 1. открыть в системном браузере
GET https://zloy.net/oauth/authorize
?response_type=code
&client_id=<app_id>
&redirect_uri=http://127.0.0.1:<порт>/zloy
&code_challenge=<base64url(sha256(verifier))>
&code_challenge_method=S256
&state=<случайное>
// 2. браузер вернёт ?code=…&state=… на локальный адрес
// 3. обменять код на токены
POST https://zloy.net/oauth/token
grant_type=authorization_code&code=…&code_verifier=…
&redirect_uri=…&client_id=…
→ {ok, access_token, expires_in, refresh_token,
user, app_id, slug, api_base, ws_url}
// 4. дальше: методы API — с Authorization: Bearer <access_token>,
// кубики — wss://api.zloy.net/rt/ws?access_token=…
// обновление доступа (старый refresh при этом гасится)
POST /oauth/token grant_type=refresh_token&refresh_token=…
// выход
POST /oauth/revoke token=<refresh_token>
// вход по коду с экрана
POST /oauth/device client_id=<app_id>
→ {device_code, user_code, verification_uri, interval}
POST /oauth/token
grant_type=urn:ietf:params:oauth:grant-type:device_code
&device_code=…
Для веб-игр — одна строка; для Godot — обычные HTTP-запросы.
JavaScript SDK добавляет подписанные параметры в каждый вызов сам.
Методы возвращают промис с разобранным JSON:
{ok: true, …} либо {ok: false, error: "…"}.
Из Godot удобнее звать REST напрямую через HTTPRequest —
строка запроса та же самая.
Базовый адрес: https://api.zloy.net/v1
(зеркало https://zloy.net/api/v1). Все методы — GET,
ответы JSON, CORS открыт.
extends Node
var query := ""
func _ready() -> void:
query = JavaScriptBridge.eval("location.search.slice(1)", true)
call_api("getUserInfo", _on_user_info)
func call_api(method: String, cb: Callable) -> void:
var req := HTTPRequest.new()
add_child(req)
req.request_completed.connect(
func(_result, _code, _headers, body):
cb.call(JSON.parse_string(body.get_string_from_utf8()))
req.queue_free())
req.request("https://api.zloy.net/v1/%s?%s" % [method, query])
func _on_user_info(res: Dictionary) -> void:
if res.get("ok"):
print("привет, ", res.user.nickname)
<script src="https://zloy.net/assets/zloy-sdk.js"></script>
<script>
const me = await zloy.getUserInfo();
console.log('привет,', me.user.nickname);
const friends = await zloy.getUserFriends(0, 100);
console.log(friends.total, friends.friends);
</script>
import 'https://zloy.net/assets/zloy-sdk.js';
interface User {
user_id: number; username: string; nickname: string; avatar: string;
}
interface Self extends User {
lang: 'ru' | 'en'; // язык аккаунта — только у самого игрока
}
interface UserInfo { ok: boolean; user: Self }
interface FriendsPage {
ok: boolean; total: number; offset: number; limit: number; friends: User[];
}
declare const zloy: {
getUserInfo(): Promise<UserInfo>;
getUserFriends(offset?: number, limit?: number): Promise<FriendsPage>;
getUserFriendsInApp(offset?: number, limit?: number): Promise<FriendsPage>;
getToken(): Promise<{ok: boolean; token: string; expires_in: number}>;
postResult(text: string): Promise<{ok: boolean; url: string; seq: number}>;
pay(description: string, amount: number): Promise<{ok: boolean; error?: string}>;
rt: RealtimeApi; // Zloy Backend, см. /docs/backend
};
const me: UserInfo = await zloy.getUserInfo();
Одинаковая во всех методах. nickname — отображаемое имя,
username — адрес страницы игрока
(zloy.net/<username>), avatar —
абсолютная ссылка либо пустая строка.
У самого игрока (getUserInfo)
есть ещё lang — язык его аккаунта на платформе,
"ru" или "en". Открывайте игру на нём, а не
по языку браузера: человек уже выбрал язык на zloy.net, и в игре
ожидает тот же. В списках друзей поля нет — чужой язык игре не нужен.
{
"user_id": 42,
"username": "zloysega",
"nickname": "Сергей",
"avatar": "https://zloy.net/uploads/avatars/u42-abc123.png",
"lang": "ru" # только в getUserInfo
}
{
"user_id": 42,
"username": "zloysega",
"nickname": "Сергей",
"avatar": "https://zloy.net/uploads/avatars/u42-abc123.png",
"lang": "ru" // только в getUserInfo
}
interface User {
user_id: number;
username: string;
nickname: string;
avatar: string; // '' если аватар не задан
}
interface Self extends User {
lang: 'ru' | 'en'; // язык аккаунта; есть только у самого игрока
}
Обязательные параметры каждого запроса —
app_id, user_id, ts, sign.
getUserInfoGET#Профиль игрока, открывшего игру. Спрашивать его при старте — нормальная практика: имя, аватар и язык обычно нужны сразу.
lang — язык аккаунта игрока ("ru" или
"en"). Это его выбор на платформе, поэтому он точнее
языка браузера и системы: включайте локализацию игры по нему.
call_api("getUserInfo", func(res):
if res.get("ok"):
$Name.text = res.user.nickname
_load_avatar(res.user.avatar)
TranslationServer.set_locale(res.user.get("lang", "ru")))
const {user} = await zloy.getUserInfo();
nameLabel.textContent = user.nickname;
if (user.avatar) avatarImg.src = user.avatar;
setLocale(user.lang); // 'ru' | 'en' — выбор игрока на платформе
// Ответ:
// { "ok": true, "user": { "user_id": 42, "username": "zloysega",
// "nickname": "Сергей", "avatar": "…", "lang": "ru" } }
const {user}: UserInfo = await zloy.getUserInfo();
render(user);
i18n.locale = user.lang; // 'ru' | 'en'
getUserFriendsGET#Друзья игрока с пагинацией: offset (по умолчанию 0)
и limit (по умолчанию и максимум 100). В ответе есть
total — сколько всего.
call_api("getUserFriends&offset=0&limit=100", func(res):
for f in res.friends:
_add_friend_row(f.nickname, f.avatar))
const {total, friends} = await zloy.getUserFriends(0, 100);
console.log(`друзей: ${total}`);
// Ответ:
// { "ok": true, "total": 152, "offset": 0, "limit": 100,
// "friends": [ { "user_id": 7, "nickname": "Алиса", … } ] }
const page: FriendsPage = await zloy.getUserFriends(0, 100);
page.friends.forEach(renderFriend);
getUserFriendsInAppGET#То же самое, но только друзья, которые уже установили вашу игру. Годится для «позови своих» и для показа, с кем можно сыграть прямо сейчас.
call_api("getUserFriendsInApp", func(res):
$InviteList.set_friends(res.friends))
const {friends} = await zloy.getUserFriendsInApp();
showInviteList(friends);
const {friends}: FriendsPage = await zloy.getUserFriendsInApp();
getUserInstalledAppsGET#Игры, которые игрок установил. Пагинация как у друзей. Полезно для кросс-промо между вашими играми.
call_api("getUserInstalledApps", func(res):
for a in res.apps:
_add_promo(a.name, a.icon, a.url))
const {apps} = await zloy.call('getUserInstalledApps');
// Ответ:
// { "ok": true, "total": 3, "offset": 0, "limit": 100,
// "apps": [ { "app_id": 1, "slug": "darkrunner", "name": "Dark Runner",
// "icon": "https://zloy.net/uploads/appicons/a1-abc.png",
// "summary": "Бегай во тьме.",
// "url": "https://zloy.net/darkrunner" } ] }
interface App {
app_id: number; slug: string; name: string;
icon: string; summary: string; url: string;
}
interface AppsPage { ok: boolean; total: number; apps: App[] }
postResultGET#Публикует результат на стене самого игрока: запись появляется по
адресу zloy.net/<игрок>/<seq>, где
seq — номер записи в пределах страницы игрока
(поэтому zloy.net/alice/7 и zloy.net/bob/7
— разные записи). Автор записи — игрок; вместе с ней сохраняется
IP запроса (требование закона о соцсетях).
text — до 280 символов, обязателен. Публиковать можно
через неделю после регистрации игрока, иначе ответ
posting_locked. Часовой лимит на игрока защищает от
спама — rate_limited.
var text := "Набрал %d очков!" % score
call_api("postResult&text=" + text.uri_encode(), func(res):
if res.get("ok"):
print("запись: ", res.url)
elif res.get("error") == "posting_locked":
pass # аккаунт моложе недели — молча пропускаем
)
const r = await zloy.postResult(`Набрал ${score} очков!`);
if (r.ok) window.open(r.url, '_blank');
// Ответ:
// { "ok": true, "post_id": 812, "seq": 34,
// "url": "https://zloy.net/alice/34" }
const r: {ok: boolean; post_id: number; seq: number; url: string} =
await zloy.postResult(`Набрал ${score} очков!`);
payпо допуску#SDK показывает оверлей «Пополнить игру» с описанием и суммой в
рублях; после подтверждения создаётся платёж
(GET /v1/createPayment?amount=<₽>&description=…)
и браузер переходит по полученному url в платёжную
систему.
Платежи доступны ограниченному списку игр. Если игра не в
списке, вызов тихо возвращает
{ok: false, error: 'payments_not_allowed'} и ничего не
показывает. Проверка допуска —
GET /v1/paymentsAllowed →
{"ok":true,"allowed":true|false}.
Куда зачислять купленное (какой банк и валюта) настраивается в свойствах игры — см. кубик «Банк».
# Платёжная панель живёт в JS SDK; из Godot Web зовём через мост:
JavaScriptBridge.eval("zloy.pay('100 кристаллов', 199)", true)
const r = await zloy.pay('100 кристаллов', 199);
// r.ok === true → игрок подтвердил, идёт переход в платёжную систему
// r.error === 'payments_not_allowed' | 'cancelled'
const r: {ok: boolean; error?: 'payments_not_allowed' | 'cancelled'} =
await zloy.pay('100 кристаллов', 199);
Нужна, только если у игры есть собственный бэкенд.
Пересчитайте подпись своим app_key — сойдётся, значит
user_id, пришедшему от клиента, можно доверять.
Не хотите держать app_key на стороннем сервере —
используйте токен личности: там проверка
публичным ключом платформы, общий секрет не нужен.
import hmac, hashlib
def valid(app_id, user_id, ts, sign, app_key):
want = hmac.new(app_key.encode(),
f"{app_id}:{user_id}:{ts}".encode(),
hashlib.sha256).hexdigest()
return hmac.compare_digest(want, sign)
import {createHmac, timingSafeEqual} from 'node:crypto';
function valid({app_id, user_id, ts, sign}, appKey) {
const want = createHmac('sha256', appKey)
.update(`${app_id}:${user_id}:${ts}`).digest('hex');
return want.length === sign.length &&
timingSafeEqual(Buffer.from(want), Buffer.from(sign));
}
func valid(appID, userID, ts int64, sign, appKey string) bool {
mac := hmac.New(sha256.New, []byte(appKey))
fmt.Fprintf(mac, "%d:%d:%d", appID, userID, ts)
want := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(want), []byte(sign))
}
Для своего бэкенда, которому вы не доверяете app_key.
zloy.getToken() делает
POST https://zloy.net/studio/v1/token с теми же
подписанными параметрами и возвращает
{"ok":true,"token":"<jwt>","expires_in":300} — JWT
(EdDSA/Ed25519) на 5 минут. SDK кеширует его и обновляет сам.
Claims: iss="https://zloy.net", sub — id
игрока строкой, aud — адрес (slug) вашей игры,
iat/exp, nickname.
Ваш сервер проверяет подпись публичным ключом платформы:
GET https://zloy.net/.well-known/jwks.json (JWKS, ключ
OKP/Ed25519, alg="EdDSA", стабильный
kid). Проверяйте подпись, exp,
iss и aud.
Для мультиплеера на платформе токен не нужен: кубики проверяют личность сами.
// клиент: получить токен и отдать своему серверу
const {token} = await zloy.getToken();
socket.send(JSON.stringify({auth: token}));
# pip install pyjwt[crypto] requests
import jwt, requests
jwks = requests.get("https://zloy.net/.well-known/jwks.json").json()
key = jwt.PyJWK.from_dict(jwks["keys"][0]).key
claims = jwt.decode(token, key=key, algorithms=["EdDSA"],
issuer="https://zloy.net",
audience="адрес-вашей-игры")
user_id = int(claims["sub"])
import {createRemoteJWKSet, jwtVerify} from 'jose';
const jwks = createRemoteJWKSet(
new URL('https://zloy.net/.well-known/jwks.json'));
const {payload} = await jwtVerify(token, jwks, {
issuer: 'https://zloy.net',
audience: 'адрес-вашей-игры',
});
const userId = Number(payload.sub);
Ответ всегда JSON; ошибка — {"ok": false, "error": "код"}:
bad_request | не хватает параметров |
expired | подпись старше 48 часов — перезагрузите страницу игры |
unknown_app | неизвестный app_id |
bad_sign | подпись не сошлась (например, ключ отозван) |
unknown_user | игрок не найден |
posting_locked | аккаунт моложе недели (только postResult) |
rate_limited | слишком часто |
Вкладку с игрой держат открытой сутками, а подпись живёт 48 часов —
обработайте expired предложением обновить страницу, а не
общей ошибкой.
call_api("getUserInfo", func(res):
if not res.get("ok"):
match res.get("error"):
"expired": _ask_reload()
_: push_warning("API: %s" % res.error))
const r = await zloy.getUserInfo();
if (!r.ok) {
if (r.error === 'expired') askReload();
else console.warn('API:', r.error);
}
type ApiError = 'bad_request' | 'expired' | 'unknown_app'
| 'bad_sign' | 'unknown_user' | 'posting_locked' | 'rate_limited';
const r = await zloy.getUserInfo();
if (!r.ok) handle((r as {error: ApiError}).error);
limit в списках — максимум 100 за запрос.postResult: до 280 символов, не раньше недели с
регистрации игрока, с часовым лимитом.app_key не должен попадать в код игры: подписи
выдаёт платформа, ключ живёт только на сервере.s.zloy.net —
чужой код не делит origin с сессией сайта.