← Партнёрская программа

API для партнёров

Базовый адрес https://persshop.com/api/v1. Всё общение — JSON. Суммы в сомони, две цифры после запятой.

Ключ

Ключ выдаётся после одобрения заявки и выглядит как ps_live_abc123.секрет. Он показывается один раз — храните его на сервере, а не в коде бота, который лежит в общем репозитории. Если ключ утёк, напишите в чат: старый отзовём, выдадим новый.

Authorization: Bearer ps_live_abc123.ваш_секрет

Не больше 120 запросов в минуту на ключ. Сверх лимита — 429 с полем retryAfterSeconds.

Ошибки

У любой ошибки есть машинный code и человеческий error. Разбирайте первый: тексты мы можем переписать, коды — нет.

{ "ok": false, "code": "insufficient_balance", "error": "Недостаточно средств на балансе." }

Ручки

GET /api/v1/catalog

Список активных категорий. Параметры: q — поиск по названию, limit, offset. Цен здесь нет — они зависят от позиции, а не от категории. Картинки — полными адресами: imageUrl — обложка, iconUrl — квадратная иконка игры (бывает null).

GET /api/v1/catalog/{id}

Номиналы и поля заказа. У каждой позиции две цены: priceTjs — ваша, retailPriceTjs — наша розница. idCheckField непустое — значит, у этой игры можно спросить ник до оплаты (см. /check-id). У каждой позиции —imageUrl, картинка номинала как на нашей витрине (где своей нет — иконка или обложка игры).
{
  "ok": true,
  "category": {
    "id": "free-fire",
    "kind": "topup",
    "title": "Free Fire",
    "imageUrl": "https://persshop.com/covers/free-fire.webp",
    "iconUrl": "https://persshop.com/game-icons/free-fire.webp",
    "fields": [{ "key": "player_id", "label": "ID игрока", "type": "text" }],
    "idCheckField": "player_id",
    "offers": [
      { "offerId": "110_diamonds", "title": "110 Diamonds",
        "priceTjs": 8.75, "retailPriceTjs": 9.95,
        "imageUrl": "https://persshop.com/items/ff/diamonds-1.webp" }
    ],
    "steamTopup": null
  }
}

GET /api/v1/balance

Сколько денег на счёте и какая у вас доля наценки. Баланс тот же, что в личном кабинете — пополняется переводом на карту на persshop.com.

POST /api/v1/orders

Оформить и сразу выполнить заказ. Обязателен заголовок Idempotency-Key — свой на каждый заказ, от 8 до 128 символов.
POST /api/v1/orders
Authorization: Bearer ps_live_abc123.секрет
Idempotency-Key: 7f0c1e2a-2c1b-4a55-9f10-3e4d5c6b7a80
Content-Type: application/json

{
  "categoryId": "free-fire",
  "offerId": "110_diamonds",
  "fields": { "player_id": "123456789" }
}
Для пополнения Steam вместо offerId шлите сумму:
{ "categoryId": "steam-topup", "amountUsd": 20, "fields": { "login": "steamlogin" } }
В ответ — заказ со статусом. Для подарочных карт и ключей код лежит в delivery.
{
  "ok": true,
  "order": {
    "id": "0f2c…", "number": 128,
    "categoryId": "free-fire", "offerId": "110_diamonds",
    "priceTjs": 8.75, "status": "fulfilled",
    "delivery": null, "error": null
  }
}

GET /api/v1/orders/{id}

Статус заказа. Принимает и UUID, и короткий номер.

GET /api/v1/orders

Ваши заказы страницами — для сверки со своей базой. Параметры: status (fulfilling, fulfilled, refunded, failed), limit (до 100), offset. В ответе total — сколько их всего, по нему видно, всё ли выкачано.
{ "ok": true, "total": 412, "orders": [ { "id": "0f2c…", "number": 128, … } ] }

POST /api/v1/check-id

Проверить игровой ID до оплаты и показать клиенту ник. Доступна тем категориям, у которых в /catalog/{id} заполнено поле idCheckField.
POST /api/v1/check-id
{ "categoryId": "free-fire", "fields": { "player_id": "123456789" } }

{ "ok": true, "valid": true, "playerName": "PlayerOne", "region": "SG" }
valid: false — такого игрока нет, пусть клиент проверит номер. 503 check_unavailable_now — сломалась проверка на стороне поставщика; это не значит, что ID неверный, и заказ в этом случае оформлять можно. 400 check_unavailable — эта игра проверку не поддерживает вовсе.

Вебхуки

Поставщик редко выдаёт мгновенно: ответ на заказ часто приходит со статусом fulfilling, а «выдано» случается через минуту, а иногда через полчаса. Чтобы не опрашивать нас по кругу, дайте адрес — мы постучимся сами, когда заказ закончится.

PUT /api/v1/webhook
{ "url": "https://ваш-сервер/persshop-hook" }

{ "ok": true, "webhook": { "url": "https://ваш-сервер/persshop-hook",
  "secret": "whsec_…", "lastAttemptAt": null, "lastResult": null, "failsInARow": 0 } }

Адрес обязан быть https и внешним. Приходит тот же объект заказа, что и в ответе на /orders:

POST на ваш адрес
X-PersShop-Signature: t=1788440698,v1=1a914c25…

{ "event": "order.fulfilled",
  "sentAt": "2026-09-03T10:00:00.000Z",
  "order": { "id": "0f2c…", "number": 128, "status": "fulfilled", "delivery": … } }

Проверьте подпись. Возьмите t и v1 из заголовка, посчитайте HMAC-SHA256 от строки <t>.<тело запроса как есть> вашим secret и сравните с v1. Отбрасывайте всё, где t старше пяти минут, — иначе перехваченное сообщение можно прислать вам повторно.

// Node.js
const [t, v1] = header.split(",").map((p) => p.split("=")[1]);
const mine = crypto.createHmac("sha256", secret).update(t + "." + raw).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(mine, "hex"));

Про Idempotency-Key

Рано или поздно ваш запрос оборвётся по таймауту уже после того, как мы списали деньги и заказали у поставщика. Ключ идемпотентности — то, что делает повтор безопасным.

Что важно знать до интеграции