Игры Стрекозы — документация для разработчиков · API v1 · бета

Своя игра в сети «Стрекоза»

Разместите браузерную игру под доменом dragonfly-flash.com: игроки входят своим аккаунтом Стрекозы, игра сразу появляется на сайте и в приложении Android, покупки внутри игры принимаем через Robokassa, а ваша доля копится в кабинете и выплачивается вам.

Платформа в бета-режиме: подключаем разработчиков по заявкам. Заявка — на сайте: Пуск → Игры → Разработчикам. После одобрения откроется кабинет разработчика.

Как это устроено

Игра работает на вашем сервере. У нас — страница https://dragonfly-flash.com/play/<адрес-игры>, которая показывает вашу игру в рамке (iframe) и даёт ей мост к Стрекозе. Вы обновляете игру у себя когда угодно — нам ничего перезапускать не нужно.

Игрок Стрекоза (dragonfly-flash.com) Ваш сервер игры ───── ────────────────────────────── ─────────────── открывает /play/my-game ─▶ страница с рамкой ── iframe ─────────────▶ ваша страница + SDK │ │ SDK: login() ─────────────▶ выдаёт билет (5 мин) ─────────────────────▶ билет → ваш сервер ◀── POST /v1/auth/verify (ключ) ──┘ ──▶ {player_id, nick, avatar_url} «Купить» в игре ───────────────────────────────────────────────────────▶ ваш сервер ◀── POST /v1/orders (ключ) ───────┘ SDK: pay(id) ─────────────▶ окно оплаты: тест / Robokassa оплачено ──▶ вебхук order.paid (подпись) ──▶ выдаёте покупку

Почему рамка. Код вашей игры не исполняется на нашем домене и не видит сессии игроков, а наш код не видит вашу игру. Общаемся только через SDK — это безопасно для обеих сторон.

Быстрый старт

  1. Создайте игру в кабинете: название, адрес на Стрекозе (например my-game → /play/my-game), адрес страницы игры на вашем сервере (https://…). Игра сразу появится в тестовом режиме на сайте и в приложении — видите её только вы и ваши тестеры.
  2. Выпустите тестовый ключ (вкладка «Интеграция»). Ключ вида dfk_test_… показывается один раз — положите его в настройки сервера игры (переменная окружения), никогда не в код страницы.
  3. Подключите SDK на странице игры:
    <script src="https://dragonfly-flash.com/static/devdocs/dragonfly-games.js"></script>
  4. Вход: DragonflyGames.login() → билет → ваш сервер → POST /api/dev/v1/auth/verify → игрок.
  5. Покупки: ваш сервер создаёт заказ POST /api/dev/v1/orders, страница вызывает DragonflyGames.pay(order.id), вы выдаёте покупку по вебхуку order.paid.
  6. Проверьте всё в тестовом режиме (деньги не списываются) и нажмите «Отправить на публикацию». После проверки игра появится в каталоге у всех, а в кабинете станет доступен боевой ключ dfk_live_….

Тестовый режим и отладка

  • Пока игра не опубликована, она в тестовом режиме: видят только автор, тестеры (добавляются по нику во вкладке «Тестеры») и администрация. Все платежи — тестовые: вместо Robokassa окно «Оплатить (тест)», деньги не списываются, а ваш сервер получает настоящий вебхук order.paid с "mode": "test".
  • У тестового режима свои ключи (dfk_test_…), свои заказы и свои билеты. Тестовый билет принимается только тестовым ключом, боевой — только боевым. Так тестовая покупка никогда не выдаст боевой товар.
  • Отладка с вашего компьютера. В поле «Тестовый адрес» можно указать http://localhost:3000/ — в тестовом режиме страница /play/my-game откроет в рамке игру прямо с вашего компьютера. Сервер Стрекозы при этом к вам не ходит: ваш локальный сервер сам вызывает наш API по ключу.
  • После публикации тестовый режим остаётся: https://dragonfly-flash.com/play/my-game?test=1 — для автора и тестеров.
  • Вебхуки на localhost и во внутренние сети не отправляются. Для локальной отладки используйте туннель (ngrok, cloudflared) или опрос GET /api/dev/v1/orders?status=paid.
  • Страница игры получает в адресе параметры: df_mode (test/live), df_platform (web/android), df_game (адрес игры).

SDK на странице игры

<script src="https://dragonfly-flash.com/static/devdocs/dragonfly-games.js"></script>
<script>
DragonflyGames.environment;   // 'site' | 'android' | 'standalone'

const info = await DragonflyGames.init();
// {sdk: 1, mode: 'test', platform: 'web', game: 'my-game', logged_in: true, nick: 'Вася', lang: 'ru'}

const auth = await DragonflyGames.login();
// {ticket: 'dft1.…', mode: 'test', player_id: 'p3f2a…', nick: 'Вася', expires_in: 300}
// Не входил на сайт — Стрекоза сама предложит войти. Отказался — ошибка 'not_logged_in'.

const res = await DragonflyGames.pay(orderId);
// {status: 'paid' | 'canceled' | 'pending' | 'expired', order: {...}}

DragonflyGames.fullscreen();             // «во весь экран»
DragonflyGames.exit();                   // выйти обратно в Стрекозу
DragonflyGames.on('pause',  () => {});   // приложение свернули
DragonflyGames.on('resume', () => {});
DragonflyGames.on('back',   () => {});   // Android: нажали «Назад»
</script>
Результат pay() — только для интерфейса. Покупку выдаёт ваш сервер по вебхуку (или по статусу заказа из API), а не страница: страницу игрок может подделать. Статус pending значит, что игрок закрыл окно ожидания, а оплата может прийти позже — вебхук придёт сам.

Открыли страницу игры напрямую (не на Стрекозе) — environment === 'standalone', вход и оплата отвечают ошибкой 'standalone'. Покажите в этом случае ссылку на https://dragonfly-flash.com/play/my-game.

Вход игрока

Билет из login() отправьте своему серверу, а он проверит его у Стрекозы:

POST https://dragonfly-flash.com/api/dev/v1/auth/verify
Authorization: Bearer dfk_test_…
Content-Type: application/json

{"ticket": "dft1.…"}

→ 200
{
  "player_id": "p3f2a9c01d4e5b6a7",   // постоянный id игрока в ВАШЕЙ игре
  "nick": "Вася",
  "avatar_url": "https://dragonfly-flash.com/api/avatars/…jpg",   // или null, если аватара нет
  "mode": "test",                     // test | live — совпадает с режимом ключа
  "platform": "web",                  // web | android
  "role": "tester",                   // owner | tester | admin | player
  "expires_at": 1792995532
}
  • player_id — обезличенный и постоянный: у одного человека в разных играх разные id, а id аккаунта Стрекозы вы не видите. Храните прогресс по player_id.
  • Билет живёт 5 минут. После проверки выдайте игроку свою сессию (cookie или токен вашего сервера).
  • Ник и аватар могут меняться: GET /api/dev/v1/players/{player_id} вернёт свежие.
  • avatar_url бывает null: покажите свою заглушку. Обычный <img src> работает сразу; для canvas, WebGL, Phaser, Pixi или Three.js загружайте картинку с crossOrigin = 'anonymous', иначе браузер не отдаст её в текстуру:
    const img = new Image();
    img.crossOrigin = 'anonymous';
    img.src = player.avatar_url;

Покупки

1. Ваш сервер создаёт заказ

POST https://dragonfly-flash.com/api/dev/v1/orders
Authorization: Bearer dfk_test_…
Content-Type: application/json

{
  "player_id": "p3f2a9c01d4e5b6a7",
  "order_id": "gems-100-8f1c",        // ВАШ уникальный id покупки: латиница, цифры, _ . : - (до 64)
  "amount": 149,                      // рубли: тест — от 1, боевой — от 10 до 15000
  "title": "100 кристаллов",          // увидит игрок в окне оплаты и в чеке
  "payload": {"item": "gems", "qty": 100}   // необязательно: ваши данные (до 2000 символов), вернутся в вебхуке
}

→ 200
{"id": 42, "order_id": "gems-100-8f1c", "player_id": "p3f2…", "mode": "test", "title": "100 кристаллов",
 "amount": 149.0, "currency": "RUB", "status": "created", "created_at": "…Z", "expires_at": "…Z", "paid_at": null,
 "dev_amount": 104.3, "fee_pct": 30.0, "payload": "{\"item\": \"gems\", \"qty\": 100}",
 "webhook": {"state": "none", "attempts": 0, "last_error": null}}
  • Повтор с тем же order_id, игроком и суммой вернёт тот же заказ — можно безопасно повторять запрос при сбое сети. Тот же order_id с другой суммой — ошибка 409.
  • Заказ живёт 24 часа, потом его статус — expired.
  • dev_amount — ваша доля с этого платежа.

2. Страница игры открывает оплату

const res = await DragonflyGames.pay(order.id);   // id — число из ответа сервера, не ваш order_id
if (res.status === 'paid') showSpinner('Покупка уже в пути…');

В тестовом режиме игрок увидит окно «Тестовый платёж» с кнопкой «Оплатить (тест)». В боевом — окно с суммой и переход на защищённую страницу Robokassa (карта, СБП), чек придёт игроку на почту.

3. Ваш сервер выдаёт покупку

По вебхуку order.paid (см. ниже) или, если вебхук не настроен, опросом GET /api/dev/v1/orders/{order_id} / GET /api/dev/v1/orders?status=paid&since_id=N. Выдавайте покупку один раз: запоминайте у себя order_id выданных заказов.

Вебхук и подпись

Укажите адрес вебхука в кабинете (вкладка «Интеграция», только https). Когда заказ оплачен, Стрекоза отправит:

POST https://game.example.com/dragonfly/webhook
Content-Type: application/json
X-Dragonfly-Event: order.paid
X-Dragonfly-Signature: t=1792995532,v1=5c1e…(hex)

{"event": "order.paid", "mode": "live", "game": "my-game", "sent_at": "2026-10-26T12:00:00Z",
 "order": {"id": 42, "order_id": "gems-100-8f1c", "player_id": "p3f2…", "status": "paid", "amount": 149.0,
           "paid_at": "…Z", "payload": "…", …}}

Проверка подписи: v1 = HMAC_SHA256(секрет, t + "." + тело_запроса_как_есть) в hex. Секрет whsec_… — в кабинете. Отклоняйте запросы с неверной подписью и с t старше 5 минут.

  • Ответ 2xx — доставлено. Любой другой ответ или нет ответа за 10 секунд — повторим через 10 с, 30 с, 2 мин, 10 мин, 30 мин, 1 ч, 3 ч, 6 ч, 12 ч, 24 ч. Повторную доставку можно запросить в кабинете (вкладка «Заказы»).
  • Вебхук может прийти дважды — выдавайте покупку идемпотентно по order_id.
  • Кнопка «Проверить связь» шлёт событие ping — на него тоже отвечайте 2xx.
  • Режим заказа — в поле mode: тестовые покупки не путайте с боевыми.

Приложение Android

  • Каталог «Игры» в приложении «Стрекоза XP» берётся с сервера: новая игра появляется в приложении сразу, без его обновления. Тестовая — только у автора и тестеров (с пометкой «Тест»).
  • В приложении игра открывается без рамки: страница вашей игры загружается в окне приложения, а SDK работает через встроенный мост (DragonflyGames.environment === 'android'). Код игры тот же — ничего отдельно делать не нужно.
  • Ориентация экрана — настройка «Ориентация в приложении» в кабинете (любая, альбомная, книжная).
  • Кнопка «Назад»: приложение шлёт событие back и спрашивает игрока, выйти ли из игры.
  • Боевая оплата в приложении открывает страницу Robokassa в браузере телефона; после оплаты игрок возвращается в приложение, а покупка приходит вашим вебхуком.
  • Входы из приложения видны в кабинете («Последний вход из приложения») — так вы проверите, что игра работает на телефоне.

Справочник API

Базовый адрес: https://dragonfly-flash.com/api/dev/v1. Авторизация: Authorization: Bearer <ключ>. Ответы — JSON, деньги — рубли, время — ISO 8601 в UTC (…Z). Вызывайте API только со своего сервера.

ЗапросЧто делает
GET /pingПроверка ключа: {ok, game, mode, status, server_time}
POST /auth/verifyПроверить билет входа {ticket} → игрок
GET /players/{player_id}Ник и аватар игрока
POST /ordersСоздать заказ {player_id, order_id, amount, title, payload?}
GET /orders/{order_id}Заказ по вашему order_id
GET /orders?status=paid&since_id=0&limit=100Список заказов режима ключа (сверка, опрос). since_id — последний полученный id

Статусы заказа: created → paid | canceled (игрок отказался) | expired (прошло 24 ч). Оплаченный заказ остаётся paid навсегда.

Лимит частоты — 20 запросов в секунду с одного адреса (с запасом 60). Превышение — HTTP 429, повторите через секунду.

Ошибки

Ошибка — HTTP-код и {"detail": "понятный текст по-русски"}.

КодКогда
400Неверные данные (сумма, order_id, title); боевой ключ до публикации
401Нет ключа, ключ отозван; билет неверный или истёк
403Билет другой игры или другого режима; игра заблокирована или не опубликована (для боевого ключа)
404Игрок или заказ не найден
409order_id уже использован для другой покупки
429Слишком часто
5xxСбой у нас — повторите запрос позже

Готовые примеры

Минимальная игра целиком: страница с SDK и сервер, который проверяет вход, создаёт заказ и выдаёт покупку по вебхуку. Запустите сервер на localhost:3000, укажите в кабинете тестовый адрес http://localhost:3000/ и откройте /play/<ваша-игра> на Стрекозе.

Страница игры (index.html)

<!DOCTYPE html>
<html lang="ru"><head><meta charset="utf-8"><title>Моя игра</title>
<script src="https://dragonfly-flash.com/static/devdocs/dragonfly-games.js"></script></head>
<body>
<p id="who">Загрузка…</p>
<p>Кристаллы: <b id="gems">0</b></p>
<button id="buy" disabled>Купить 100 кристаллов за 149 ₽</button>
<script>
const $ = (id) => document.getElementById(id);
let session = null;   // токен НАШЕГО сервера после входа

async function api(path, body) {
  const r = await fetch(path, {method: 'POST', headers: {'Content-Type': 'application/json',
    'Authorization': session ? 'Bearer ' + session : ''}, body: JSON.stringify(body || {})});
  const d = await r.json();
  if (!r.ok) throw new Error(d.detail || r.status);
  return d;
}

async function start() {
  if (DragonflyGames.environment === 'standalone') {
    $('who').textContent = 'Откройте игру на Стрекозе: https://dragonfly-flash.com/play/my-game';
    return;
  }
  const auth = await DragonflyGames.login();            // билет от Стрекозы
  const me = await api('/api/login', {ticket: auth.ticket});
  session = me.session;
  $('who').textContent = 'Привет, ' + me.nick + (auth.mode === 'test' ? ' (тестовый режим)' : '');
  $('gems').textContent = me.gems;
  $('buy').disabled = false;
}

$('buy').onclick = async () => {
  const order = await api('/api/buy', {item: 'gems100'});   // сервер создаёт заказ в Стрекозе
  const res = await DragonflyGames.pay(order.id);
  if (res.status === 'paid') setTimeout(refresh, 1500);     // покупку выдаст сервер по вебхуку
};

async function refresh() { const me = await api('/api/me'); $('gems').textContent = me.gems; }
start().catch((e) => { $('who').textContent = 'Ошибка: ' + e.message; });
</script>
</body></html>

Сервер на Python (FastAPI)

pip install fastapi uvicorn httpx, затем DF_KEY=dfk_test_… DF_WEBHOOK_SECRET=whsec_… uvicorn game_server:app --port 3000

# game_server.py — пример сервера игры для Стрекозы
import hashlib, hmac, json, os, secrets, time
import httpx
from fastapi import FastAPI, Header, HTTPException, Request
from fastapi.responses import FileResponse

DF_API = os.getenv("DF_API", "https://dragonfly-flash.com/api/dev/v1")
DF_KEY = os.environ["DF_KEY"]                       # dfk_test_… или dfk_live_…
DF_WEBHOOK_SECRET = os.environ["DF_WEBHOOK_SECRET"] # whsec_… из кабинета
PRICES = {"gems100": (149, "100 кристаллов", 100)}

app = FastAPI()
players = {}    # player_id -> {"nick", "gems"}         (в настоящей игре — база данных)
sessions = {}   # токен сессии -> player_id
granted = set() # order_id уже выданных покупок — выдаём один раз

def df(method, path, body=None):
    r = httpx.request(method, DF_API + path, json=body, timeout=15,
                      headers={"Authorization": "Bearer " + DF_KEY})
    if r.status_code >= 400:
        raise HTTPException(r.status_code, r.json().get("detail", "Ошибка Стрекозы"))
    return r.json()

def me(authorization):
    pid = sessions.get((authorization or "").removeprefix("Bearer "))
    if not pid:
        raise HTTPException(401, "Нет входа")
    return pid

@app.get("/")
def page():
    return FileResponse("index.html")

@app.post("/api/login")
async def login(request: Request):
    ticket = (await request.json())["ticket"]
    p = df("POST", "/auth/verify", {"ticket": ticket})
    pl = players.setdefault(p["player_id"], {"gems": 0})
    pl["nick"] = p["nick"]
    token = secrets.token_urlsafe(24)
    sessions[token] = p["player_id"]
    return {"session": token, "nick": p["nick"], "gems": pl["gems"]}

@app.post("/api/me")
def get_me(authorization: str = Header(None)):
    return players[me(authorization)]

@app.post("/api/buy")
async def buy(request: Request, authorization: str = Header(None)):
    pid = me(authorization)
    item = (await request.json())["item"]
    price, title, _ = PRICES[item]
    order_id = f"{item}-{secrets.token_hex(6)}"
    return df("POST", "/orders", {"player_id": pid, "order_id": order_id, "amount": price,
                                  "title": title, "payload": {"item": item}})

@app.post("/dragonfly/webhook")
async def webhook(request: Request, x_dragonfly_signature: str = Header("")):
    body = await request.body()
    parts = dict(x.split("=", 1) for x in x_dragonfly_signature.split(",") if "=" in x)
    good = hmac.new(DF_WEBHOOK_SECRET.encode(), parts.get("t", "").encode() + b"." + body,
                    hashlib.sha256).hexdigest()
    if not hmac.compare_digest(good, parts.get("v1", "")) or abs(time.time() - int(parts.get("t", 0))) > 300:
        raise HTTPException(401, "bad signature")
    event = json.loads(body)
    if event["event"] == "order.paid":
        o = event["order"]
        if o["order_id"] not in granted:            # выдаём ровно один раз
            item = json.loads(o["payload"])["item"]
            players.setdefault(o["player_id"], {"gems": 0, "nick": "?"})["gems"] += PRICES[item][2]
            granted.add(o["order_id"])
    return {"ok": True}

Сервер на Node.js (Express)

npm i express (Node 18+), затем DF_KEY=… DF_WEBHOOK_SECRET=… node server.js

// server.js — пример сервера игры для Стрекозы
const express = require('express');
const crypto = require('crypto');
const DF_API = process.env.DF_API || 'https://dragonfly-flash.com/api/dev/v1';
const { DF_KEY, DF_WEBHOOK_SECRET } = process.env;
const PRICES = { gems100: { amount: 149, title: '100 кристаллов', qty: 100 } };
const players = new Map(), sessions = new Map(), granted = new Set();

async function df(method, path, body) {
  const r = await fetch(DF_API + path, { method, body: body && JSON.stringify(body),
    headers: { 'Authorization': 'Bearer ' + DF_KEY, 'Content-Type': 'application/json' } });
  const d = await r.json();
  if (!r.ok) throw Object.assign(new Error(d.detail), { status: r.status });
  return d;
}
const app = express();
// вебхук — ДО express.json(): подпись считается по сырому телу
app.post('/dragonfly/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = Object.fromEntries(String(req.get('X-Dragonfly-Signature') || '').split(',').map(x => x.split('=')));
  const good = crypto.createHmac('sha256', DF_WEBHOOK_SECRET).update(sig.t + '.').update(req.body).digest('hex');
  const v1 = String(sig.v1 || '');
  if (v1.length !== good.length || !crypto.timingSafeEqual(Buffer.from(good), Buffer.from(v1))
      || Math.abs(Date.now() / 1000 - Number(sig.t)) > 300) return res.status(401).end();
  const ev = JSON.parse(req.body);
  if (ev.event === 'order.paid' && !granted.has(ev.order.order_id)) {
    const item = JSON.parse(ev.order.payload).item;
    const p = players.get(ev.order.player_id) || { gems: 0 };
    p.gems += PRICES[item].qty; players.set(ev.order.player_id, p); granted.add(ev.order.order_id);
  }
  res.json({ ok: true });
});
app.use(express.json());
app.get('/', (req, res) => res.sendFile(__dirname + '/index.html'));
const me = (req) => sessions.get(String(req.get('Authorization') || '').replace('Bearer ', ''));
app.post('/api/login', async (req, res) => {
  try {
    const p = await df('POST', '/auth/verify', { ticket: req.body.ticket });
    const pl = players.get(p.player_id) || { gems: 0 }; pl.nick = p.nick; players.set(p.player_id, pl);
    const token = crypto.randomBytes(24).toString('base64url'); sessions.set(token, p.player_id);
    res.json({ session: token, nick: p.nick, gems: pl.gems });
  } catch (e) { res.status(e.status || 500).json({ detail: e.message }); }
});
app.post('/api/me', (req, res) => me(req) ? res.json(players.get(me(req))) : res.status(401).end());
app.post('/api/buy', async (req, res) => {
  const pid = me(req); if (!pid) return res.status(401).end();
  const it = PRICES[req.body.item];
  try {
    res.json(await df('POST', '/orders', { player_id: pid, order_id: req.body.item + '-' + crypto.randomBytes(6).toString('hex'),
      amount: it.amount, title: it.title, payload: { item: req.body.item } }));
  } catch (e) { res.status(e.status || 500).json({ detail: e.message }); }
});
app.listen(3000, () => console.log('http://localhost:3000'));

Деньги и выплаты

  • Игрок платит Стрекозе через Robokassa (карта, СБП), чек ему присылает Robokassa.
  • Ваша доля считается с полной суммы платежа: стандартно 70% вам, 30% — площадке (персональная доля, если она согласована, видна в кабинете). Комиссию Robokassa площадка платит из своей доли.
  • Кабинет → «Финансы»: оборот, начислено, выплачено и сколько к выплате, по каждой игре. Тестовые платежи в расчётах не участвуют.
  • Укажите реквизиты для выплат в «Финансах». Выплаты делает администрация; каждая выплата отмечается в кабинете.
  • Возвраты и спорные платежи решаются с администрацией площадки.

Публикация и правила

  • Игра должна работать в рамке на сайте и в окне приложения, на телефоне и на компьютере.
  • Своя графика, звуки и названия; без чужих торговых марок, взрослого контента, азартных игр на деньги и сбора данных игроков сверх player_id, ника и аватара.
  • Продавать можно только внутриигровые товары и услуги вашей игры. Цены — в рублях, через SDK Стрекозы, без сторонних платёжек внутри игры.
  • Смена боевого адреса опубликованной игры возвращает её на проверку.
  • Администрация может снять игру с публикации или заблокировать её при нарушениях; вы получите уведомление с причиной.

Частые вопросы

Нужно ли мне просить вас что-то обновить на сервере, когда я выпускаю новую версию? Нет. Игра живёт у вас: обновили у себя — игроки видят новую версию. Названия, описание, значок, адреса — правятся в кабинете и применяются сразу.

Можно ли открыть игру не через Стрекозу? Можно, но вход и оплата работают только на Стрекозе (на сайте или в приложении).

Мой сервер не принимает запросы из вашего iframe. Не запрещайте встраивание: уберите X-Frame-Options или поставьте Content-Security-Policy: frame-ancestors https://dragonfly-flash.com https://www.dragonfly-flash.com. Cookie вашего сервера внутри рамки — сторонние: используйте токен в заголовке, как в примере, или cookie с SameSite=None; Secure.

Ключ утёк. Отзовите его в кабинете и выпустите новый — старый перестанет работать сразу.

Где взять помощь? Напишите администрации в личные сообщения на Стрекозе.