# Tessera API v1 — полная документация и руководство

Tessera — B2B-платформа пополнений для реселлеров. Ваш сайт продаёт пополнение покупателю и создаёт заказ в Tessera через API. Tessera списывает цену с вашего предоплаченного баланса в USDT, выполняет пополнение и сообщает результат.

- Базовый адрес API: `https://b2b-2jq.pages.dev/v1`
- Кабинет: `https://b2b-2jq.pages.dev`
- Документация в браузере: `https://b2b-2jq.pages.dev/docs`
- Этот файл: `https://b2b-2jq.pages.dev/tessera-api.md`
- OpenAPI 3.1: `https://b2b-2jq.pages.dev/v1/openapi.json`
- ИИ-коннектор (MCP): `https://b2b-2jq.pages.dev/mcp`

Все ответы — JSON в UTF-8. Все суммы — строки в USDT с точкой, например `"0.95"`. Время — ISO 8601 с часовым поясом.

## Содержание

1. [Быстрый старт: первый заказ за 15 минут](#1-быстрый-старт-первый-заказ-за-15-минут)
2. [Как устроен заказ](#2-как-устроен-заказ)
3. [Аутентификация и ключи](#3-аутентификация-и-ключи)
4. [Тестовый режим](#4-тестовый-режим)
5. [Повторы и Idempotency-Key](#5-повторы-и-idempotency-key)
6. [Методы API](#6-методы-api)
7. [Вебхуки](#7-вебхуки)
8. [Ошибки](#8-ошибки)
9. [Пополнение баланса USDT](#9-пополнение-баланса-usdt)
10. [Лимиты запросов](#10-лимиты-запросов)
11. [Версии API](#11-версии-api)
12. [Готовая интеграция: полный пример](#12-готовая-интеграция-полный-пример)
13. [Чек-лист перед запуском](#13-чек-лист-перед-запуском)
14. [Частые вопросы](#14-частые-вопросы)
15. [Заметки для ИИ-ассистентов](#15-заметки-для-ии-ассистентов)
16. [ИИ-коннектор (MCP)](#16-ии-коннектор-mcp)

---

## 1. Быстрый старт: первый заказ за 15 минут

### Шаг 1. Регистрация

1. Откройте `https://b2b-2jq.pages.dev` и нажмите «Продолжить с Google».
2. После входа аккаунт получает статус «Ожидает активации». Менеджер проверит заявку и активирует аккаунт.
3. После активации в кабинете появятся цены, баланс, заказы и API-ключи.

Документация доступна без входа.

### Шаг 2. Тестовый ключ

1. Кабинет → «Разработчику» → «API-ключи» → «Создать ключ».
2. Выберите режим **Test — песочница**. Ключ начинается с `rk_test_`.
3. Скопируйте ключ сразу: он показывается один раз. Храните его только на сервере.
4. Пополните тестовый баланс в кабинете (переключатель режима вверху → «Тест», затем раздел «Баланс»). Деньги виртуальные.

### Шаг 3. Первый запрос

```bash
curl https://b2b-2jq.pages.dev/v1/balance \
  -H "Authorization: Bearer rk_test_ВАШ_КЛЮЧ"
```

Ответ:

```json
{ "balance": "100.00", "currency": "USDT", "mode": "test", "updated_at": "2026-10-01T10:00:00+00:00" }
```

### Шаг 4. Каталог

```bash
curl https://b2b-2jq.pages.dev/v1/products \
  -H "Authorization: Bearer rk_test_ВАШ_КЛЮЧ"
```

Запомните `product_id` нужного товара, например `ff.cis.dia.110`.

### Шаг 5. Тестовый заказ

```bash
curl https://b2b-2jq.pages.dev/v1/orders \
  -H "Authorization: Bearer rk_test_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: test-0001" \
  -d '{"product_id":"ff.cis.dia.110","fields":{"player_id":"10000001"}}'
```

Player ID `10000001` в тестовом режиме всегда выполняется успешно. Через несколько секунд проверьте статус:

```bash
curl https://b2b-2jq.pages.dev/v1/orders/1 \
  -H "Authorization: Bearer rk_test_ВАШ_КЛЮЧ"
```

Статус `completed` — готово. Теперь проверьте неудачный заказ с `10000002`: он завершится `failed`, а сумма вернётся на тестовый баланс.

### Шаг 6. Вебхуки

Кабинет → «Разработчику» → «Вебхуки»: укажите HTTPS-адрес вашего сервера и сохраните секрет подписи. Реализуйте проверку подписи по разделу [7](#7-вебхуки). Кнопка «Отправить тестовое событие» пришлёт событие `webhook.test`.

### Шаг 7. Боевой режим

1. Пополните боевой баланс в USDT (раздел [9](#9-пополнение-баланса-usdt)).
2. Создайте ключ в режиме **Live — реальные деньги** — он начинается с `rk_live_`.
3. Замените тестовый ключ на боевой. Код менять не нужно: API в обоих режимах одинаковый.

---

## 2. Как устроен заказ

```
Ваш сайт                         Tessera
   │  GET  /v1/products  ───────────▶  каталог и ваши цены
   │  POST /v1/players/validate ────▶  ник и регион игрока (по желанию)
   │  POST /v1/orders  ─────────────▶  списание с баланса, статус processing
   │                                   … выполнение пополнения …
   │  ◀──────────── вебхук order.completed / order.failed
   │  GET  /v1/orders/{number} ─────▶  статус в любой момент
```

### Статусы заказа

| Статус | Что значит | Деньги |
|---|---|---|
| `processing` | заказ принят, пополнение выполняется | списаны |
| `completed` | пополнение выполнено | списаны окончательно |
| `failed` | пополнение не выполнено | **уже возвращены** на баланс, в заказе есть поле `refund` |

Правила, на которые можно опираться:

- `failed` всегда означает, что возврат уже сделан. Ничего запрашивать не нужно.
- `processing` может длиться дольше обычного, если результат у исполнителя ещё не известен. Деньги не возвращаются, пока нет уверенности, что пополнение не прошло. Это защищает и вас, и покупателя от двойного пополнения.
- Финальный статус (`completed` или `failed`) больше не меняется.
- Возврат по заказу делается не больше одного раза.

### Причины неудачи (`failure.code`)

| Код | Значение |
|---|---|
| `fulfilment_failed` | пополнение не удалось выполнить |
| `invalid_player_id` | Player ID отклонён при выполнении |
| `product_unavailable` | товар стал недоступен |
| `service_unavailable` | заказ не удалось обработать вовремя |

Во всех случаях сумма возвращена на баланс.

---

## 3. Аутентификация и ключи

Каждый запрос передаёт ключ в заголовке:

```
Authorization: Bearer rk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Допускается и заголовок `X-API-Key: rk_…`.

| Префикс | Режим |
|---|---|
| `rk_live_…` | боевой: реальный баланс, реальные пополнения |
| `rk_test_…` | песочница: виртуальный баланс, результат задаётся Player ID |

Формат ключа: `rk_(live|test)_` и 40 латинских букв и цифр.

Правила безопасности:

- Храните ключ только на сервере. Не вставляйте его в JavaScript сайта, мобильное приложение или публичный репозиторий.
- Для ключа можно задать список разрешённых IP-адресов. Запрос с другого адреса получит `403 ip_not_allowed`.
- Отзыв ключа действует со следующего запроса.
- Если ключ утёк, создайте новый и отзовите старый в кабинете.

Права ключа: `catalog:read`, `orders:read`, `orders:write`, `balance:read`. По умолчанию выдаются все.

---

## 4. Тестовый режим

С ключом `rk_test_…` все методы работают так же, как в боевом режиме, но деньги виртуальные и до игры ничего не доходит. Исход заказа задаётся Player ID:

| Player ID | Результат |
|---|---|
| `10000001` | выполняется через несколько секунд |
| `10000002` | не выполняется, сумма возвращается на тестовый баланс |
| `10000003` | около 90 секунд в статусе `processing`, затем выполняется |
| `10000004` | игрок не существует: `invalid_player_id`, ничего не списано |
| `10000005` | первая попытка с неизвестным результатом, затем выполняется |
| `10000006` | игрока не удаётся проверить: `player_unverifiable`, ничего не списано |
| любой другой | выполняется через несколько секунд |

Номера тестовых заказов идут отдельно от боевых и начинаются с 1. Тестовые и боевые данные никогда не смешиваются.

---

## 5. Повторы и Idempotency-Key

`POST /v1/orders` требует заголовок `Idempotency-Key`: уникальную строку на одну покупку, от 1 до 255 печатных ASCII-символов. Удобно использовать номер заказа на вашем сайте, например `shop-58121`.

| Ситуация | Результат |
|---|---|
| тот же ключ и то же тело | вернётся исходный заказ, HTTP `200`, заголовок `Idempotent-Replayed: true`; второго списания не будет |
| тот же ключ, другое тело | `409 idempotency_conflict` |
| запрос завершился ошибкой (например, `insufficient_balance`) | заказ не создан, тот же ключ можно использовать снова |

**При таймауте, обрыве соединения или ответе 5xx повторите запрос с тем же ключом.** Это всегда безопасно. Никогда не создавайте новый ключ для повтора той же покупки, иначе получите два заказа.

---

## 6. Методы API

Все пути указаны относительно `https://b2b-2jq.pages.dev`.

### GET /v1/products — каталог и цены

Товары, доступные вам прямо сейчас, с вашими ценами.

```json
{
  "data": [{
    "product_id": "ff.cis.dia.110",
    "game": { "id": "ff", "name": "Free Fire" },
    "region": { "id": "cis", "name": "CIS" },
    "group": "diamonds",
    "title": "110 Diamonds",
    "description": null,
    "price": "0.95",
    "currency": "USDT",
    "fields": [
      { "key": "player_id", "label": "Player ID", "type": "string",
        "pattern": "^[0-9]{6,15}$", "required": true }
    ]
  }]
}
```

- `product_id` постоянный и никогда не переиспользуется. Его можно хранить в своей базе.
- `fields` описывает, что передать при заказе. Проверяйте значение по `pattern` ещё до отправки.
- Временно недоступный товар исчезает из списка, а заказ на него вернёт `422 product_unavailable`.
- Цены могут меняться. Загружайте каталог регулярно, например раз в 5–10 минут, или передавайте `expected_price` при заказе.

### GET /v1/products/{product_id} — один товар

Ответ — один объект товара в том же формате. Неизвестный товар: `404 product_not_found`.

### POST /v1/players/validate — проверка игрока

Проверяет, что Player ID существует, и возвращает ник и регион. Деньги не списываются. Удобно показать покупателю ник до оплаты.

Запрос:

```json
{ "product_id": "ff.cis.dia.110", "fields": { "player_id": "1234567890" } }
```

Ответ:

```json
{ "valid": true, "player": { "id": "1234567890", "name": "Nick_RU", "region": "RU" } }
```

Ошибки: `422 invalid_player_id` (игрок не найден), `422 player_unverifiable` (проверить сейчас не удалось, повторите позже), `422 player_region_mismatch` (регион аккаунта не подходит товару).

Вызывать этот метод перед заказом не обязательно: создание заказа проверяет игрока само.

### POST /v1/orders — создание заказа

Заголовки:

| Заголовок | Обяз. | Значение |
|---|---|---|
| `Authorization` | да | `Bearer rk_…` |
| `Content-Type` | да | `application/json` |
| `Idempotency-Key` | да | уникален для каждой покупки, до 255 символов |

Тело:

| Поле | Обяз. | Описание |
|---|---|---|
| `product_id` | да | товар из каталога |
| `fields` | да | объект полей товара, например `{"player_id":"1234567890"}`; все значения — строки |
| `client_order_id` | нет | номер заказа на вашей стороне, 1–64 печатных ASCII-символа, уникален в пределах аккаунта |
| `expected_price` | нет | строка, например `"0.95"`; если цена другая — `409 price_changed`, ничего не списано |

Другие поля в теле запрещены: `400 validation_error`.

Пример:

```bash
curl https://b2b-2jq.pages.dev/v1/orders \
  -H "Authorization: Bearer rk_live_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: shop-58121" \
  -d '{
    "product_id": "ff.cis.dia.110",
    "fields": { "player_id": "1234567890" },
    "client_order_id": "58121",
    "expected_price": "0.95"
  }'
```

Ответ `201 Created` (или `200` при повторе с тем же ключом) содержит заказ со статусом `processing` и остаток баланса:

```json
{
  "order": {
    "number": 1042,
    "mode": "live",
    "status": "processing",
    "product_id": "ff.cis.dia.110",
    "price": "0.95",
    "currency": "USDT",
    "fields": { "player_id": "1234567890" },
    "player": { "name": "Nick_RU", "region": "RU" },
    "client_order_id": "58121",
    "failure": null,
    "refund": null,
    "created_at": "2026-10-01T10:15:02.114+00:00",
    "finished_at": null
  },
  "balance": "245.10"
}
```

Перед списанием сервер проверяет: формат полей, игрока, цену (`expected_price`), баланс и дневной лимит. Если любая проверка не прошла, заказ не создаётся и ничего не списывается.

### GET /v1/orders/{number} — статус заказа

Заказ с историей событий `timeline`:

```json
{
  "number": 1042,
  "mode": "live",
  "status": "completed",
  "product_id": "ff.cis.dia.110",
  "price": "0.95",
  "currency": "USDT",
  "fields": { "player_id": "1234567890" },
  "player": { "name": "Nick_RU", "region": "RU" },
  "client_order_id": "58121",
  "failure": null,
  "refund": null,
  "created_at": "2026-10-01T10:15:02.114+00:00",
  "finished_at": "2026-10-01T10:15:09.870+00:00",
  "timeline": [
    { "type": "accepted",  "at": "…" },
    { "type": "charged",   "at": "…", "amount": "0.95" },
    { "type": "sent",      "at": "…" },
    { "type": "completed", "at": "…" }
  ]
}
```

У неудачного заказа: `"status": "failed"`, `"failure": { "code": "fulfilment_failed", "message": "…" }`, `"refund": { "amount": "0.95", "at": "…" }`.

Чужой или несуществующий заказ: `404 order_not_found`.

Если вы опрашиваете статус вместо вебхуков, делайте это не чаще раза в 3–5 секунд.

### GET /v1/orders — список заказов

Параметры запроса:

| Параметр | Описание |
|---|---|
| `status` | `processing`, `completed` или `failed` |
| `limit` | 1–100, по умолчанию 50 |
| `cursor` | значение `next_cursor` из предыдущего ответа |
| `client_order_id` | найти заказ по вашему номеру (остальные параметры игнорируются) |

Сортировка — от новых к старым. Ответ: `{ "data": [ …заказы… ], "next_cursor": "1001" }`. Когда `next_cursor` равен `null`, страниц больше нет.

### GET /v1/balance — баланс

```json
{ "balance": "246.05", "currency": "USDT", "mode": "live", "updated_at": "…" }
```

### GET /v1/ledger — выписка

Все движения по балансу, от новых к старым, с остатком после каждой операции. Параметры `limit` и `cursor` — как у списка заказов.

```json
{
  "data": [
    { "id": 81, "kind": "order_refund", "amount": "0.95", "balance_after": "246.05",
      "order_number": 1041, "note": null, "created_at": "…" },
    { "id": 80, "kind": "order_charge", "amount": "-0.95", "balance_after": "245.10",
      "order_number": 1041, "note": null, "created_at": "…" }
  ],
  "next_cursor": "80"
}
```

Виды операций (`kind`):

| kind | Что это |
|---|---|
| `crypto_deposit` | пополнение USDT |
| `manual_credit` / `manual_debit` | ручное зачисление или списание менеджером |
| `order_charge` | оплата заказа (сумма отрицательная) |
| `order_refund` | возврат за неудачный заказ |
| `test_credit` / `crypto_test_credit` | пополнение тестового баланса |

### GET /v1/me — аккаунт

Режим ключа, статус аккаунта и баланс.

### GET /v1/openapi.json — машиночитаемое описание

Спецификация OpenAPI 3.1. Подходит для генерации клиентов и для ИИ-инструментов.

---

## 7. Вебхуки

Адрес задаётся в кабинете: «Разработчику» → «Вебхуки». Tessera отправляет `POST` с JSON, когда заказ достигает финального статуса.

| Событие | Когда |
|---|---|
| `order.completed` | заказ выполнен |
| `order.failed` | заказ не выполнен, сумма возвращена |
| `webhook.test` | тестовое событие из кабинета |

### Запрос

```
POST /hooks/tessera HTTP/1.1
Content-Type: application/json
User-Agent: Tessera-Webhooks/1
X-Event-Id: evt_4f1c2a9e0b7d4c1e9a3b5c7d9e1f3a5b
X-Event-Type: order.completed
X-Delivery-Attempt: 1
X-Signature: t=1790815200,v1=5f2c…

{
  "id": "evt_4f1c2a9e0b7d4c1e9a3b5c7d9e1f3a5b",
  "type": "order.completed",
  "created_at": "2026-10-01T10:15:09.870+00:00",
  "mode": "live",
  "data": { "order": { "number": 1042, "status": "completed", "…": "…" } }
}
```

`data.order` — заказ в том же формате, что `GET /v1/orders/{number}`, но без `timeline`.

### Проверка подписи

`X-Signature: t=<unix-время>,v1=<hex>`, где `v1 = HMAC-SHA256(секрет, t + "." + сырое_тело)`.

1. Считайте подпись от **сырого** тела запроса, до разбора JSON.
2. Сравнивайте за постоянное время.
3. Отклоняйте события, у которых `t` отличается от текущего времени больше чем на 5 минут.

### Доставка и повторы

- Ответьте кодом 2xx за 10 секунд. Долгую обработку делайте после ответа.
- При ошибке или таймауте Tessera повторит через 1 мин, 5 мин, 30 мин, 2 ч, 6 ч, 12 ч и 24 ч.
- `X-Event-Id` одинаков во всех повторах. Сохраняйте обработанные id, чтобы не обработать событие дважды.
- После 50 неудач подряд доставка отключается. Включить снова: сохраните адрес в кабинете ещё раз.
- Вебхук — уведомление. Источник истины — `GET /v1/orders/{number}`. При сомнениях запросите заказ.

### Node.js (Express)

```js
import crypto from "node:crypto";
import express from "express";

const app = express();

app.post("/hooks/tessera", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("X-Signature") ?? "";
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", process.env.TESSERA_WEBHOOK_SECRET)
    .update(`${t}.${req.body}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  if (!fresh || !v1 || v1.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(req.body);
  res.sendStatus(200);
  // обработка после ответа: event.type, event.data.order
});
```

### PHP

```php
<?php
$raw = file_get_contents("php://input");
$header = $_SERVER["HTTP_X_SIGNATURE"] ?? "";
parse_str(str_replace(",", "&", $header), $sig);
$expected = hash_hmac("sha256", ($sig["t"] ?? "") . "." . $raw, getenv("TESSERA_WEBHOOK_SECRET"));
if (abs(time() - (int)($sig["t"] ?? 0)) > 300 || !hash_equals($expected, $sig["v1"] ?? "")) {
  http_response_code(401);
  exit;
}
$event = json_decode($raw, true);
http_response_code(200);
// $event["type"], $event["data"]["order"]["number"]
```

### Python (Flask)

```python
import hashlib, hmac, os, time
from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/hooks/tessera")
def hook():
    raw = request.get_data()
    parts = dict(p.split("=", 1) for p in request.headers.get("X-Signature", "").split(",") if "=" in p)
    expected = hmac.new(os.environ["TESSERA_WEBHOOK_SECRET"].encode(),
                        f"{parts.get('t')}.".encode() + raw, hashlib.sha256).hexdigest()
    if abs(time.time() - int(parts.get("t", 0))) > 300 or \
            not hmac.compare_digest(expected, parts.get("v1", "")):
        abort(401)
    event = request.get_json()
    return "", 200
```

---

## 8. Ошибки

Все ошибки имеют один формат:

```json
{
  "error": {
    "code": "insufficient_balance",
    "message": "The balance is too low for this order.",
    "request_id": "req_7Hq2mV0aX9cD1eF3gH5j",
    "details": { "balance": "0.50", "price": "0.95" }
  }
}
```

Ориентируйтесь на `code`: он не меняется. `message` может меняться. `request_id` есть в каждом ответе и в заголовке `X-Request-Id` — сообщайте его в поддержку.

| HTTP | code | Что делать |
|---|---|---|
| 400 | `invalid_json` | исправьте тело запроса |
| 400 | `validation_error` | неверное поле; подробности в `details.field` и `details.reason` |
| 400 | `missing_idempotency_key` | добавьте заголовок `Idempotency-Key` |
| 401 | `missing_api_key` | передайте ключ в `Authorization: Bearer rk_…` |
| 401 | `invalid_api_key` | ключ неверный или отозван |
| 402 | `insufficient_balance` | пополните баланс; заказ не создан |
| 403 | `account_blocked` | аккаунт заблокирован; причина в `details.reason` |
| 403 | `account_pending` | аккаунт ещё не активирован |
| 403 | `ip_not_allowed` | запрос с IP вне списка разрешённых для ключа |
| 403 | `insufficient_scope` | у ключа нет нужного права |
| 404 | `not_found` / `product_not_found` / `order_not_found` | объект не существует или чужой |
| 405 | `method_not_allowed` | неверный HTTP-метод |
| 409 | `idempotency_conflict` | этот `Idempotency-Key` уже использован с другим телом |
| 409 | `price_changed` | цена отличается от `expected_price`; новая цена в `details` |
| 409 | `duplicate_client_order_id` | заказ с таким `client_order_id` уже есть |
| 409 | `too_many_open_invoices` | слишком много открытых счетов на пополнение |
| 413 | `payload_too_large` | слишком большое тело запроса |
| 415 | `unsupported_media_type` | отправляйте `Content-Type: application/json` |
| 422 | `product_unavailable` | товар временно недоступен |
| 422 | `invalid_player_id` | игрок не существует; ничего не списано |
| 422 | `player_unverifiable` | игрока сейчас не удалось проверить; ничего не списано, повторите позже |
| 422 | `player_region_mismatch` | регион аккаунта игрока не подходит товару |
| 422 | `daily_limit_exceeded` | достигнут дневной лимит расходов |
| 422 | `crypto_network_unavailable` | пополнение в этой сети недоступно |
| 429 | `rate_limited` | подождите `Retry-After` секунд |
| 500 | `internal_error` | повторите позже с тем же `Idempotency-Key` |
| 503 | `temporarily_unavailable` | приём заказов временно приостановлен; ничего не списано |

Какие ошибки можно повторять:

- **Можно повторять с тем же `Idempotency-Key`:** сетевые ошибки, таймауты, `429`, `500`, `503`, `player_unverifiable`.
- **Повторять бесполезно, пока не исправлена причина:** все остальные `4xx`.

---

## 9. Пополнение баланса USDT

Баланс пополняется в кабинете: раздел «Пополнить». Деньги отправляются напрямую из вашего кошелька.

| Сеть | Токен |
|---|---|
| TRON (TRC-20) | USDT, контракт `TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` |
| BNB Smart Chain (BEP-20) | Binance-Peg BSC-USD, 1:1 к USDT, контракт `0x55d398326f99059fF775485246999027B3197955` |
| TON | USDT, мастер `EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs`, обязателен комментарий из счёта |
| Aptos | USDt, метаданные `0x357b0b74bc833e95a115ad22604854d6b0fca151cecd94111770e5d6ffc9dc2b` |

Набор доступных сетей показывается в кабинете и может отличаться.

Порядок:

1. Выберите сеть и сумму — получите счёт: адрес, **точную** сумму (к ней добавлено несколько тысячных) и, для TON, комментарий.
2. Отправьте **ровно эту сумму** в выбранной сети в течение 30 минут. Учтите, что биржа может удержать комиссию из суммы перевода — сумма зачисления должна совпасть точно.
3. После подтверждения в сети (обычно 1–3 минуты) сумма зачисляется автоматически и появляется в выписке как `crypto_deposit`.

Автоматически **не** зачисляются: другая сумма, перевод после истечения счёта, перевод в TON без комментария, другой токен или другая сеть. Такие платежи проверяет менеджер — сообщите ему код счёта и хеш транзакции. Одна транзакция не может быть зачислена дважды.

---

## 10. Лимиты запросов

Лимиты считаются на ключ в минуту и не мешают друг другу: частый опрос статуса не блокирует создание заказов.

| Группа | Лимит | Методы |
|---|---|---|
| каталог | 120 / мин | `GET /products` |
| создание заказов | 30 / мин | `POST /orders` |
| чтение заказов | 120 / мин | `GET /orders` |
| аккаунт | 60 / мин | `/balance`, `/ledger`, `/me` |
| проверка игрока | 60 / мин | `POST /players/validate` |

При превышении — `429 rate_limited` и заголовок `Retry-After` (секунды). Остаток — в заголовках `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`. Для большего объёма свяжитесь с менеджером.

---

## 11. Версии API

- Версия указана в пути: `/v1`.
- Внутри версии добавляется только новое: поля, товары, коды ошибок. Не полагайтесь на порядок полей и игнорируйте незнакомые поля и коды.
- Несовместимые изменения выйдут как `/v2`. О выводе старой версии предупредим заранее и добавим заголовки `Deprecation` и `Sunset`.

---

## 12. Готовая интеграция: полный пример

Ниже — минимальный, но полный модуль для сервера магазина на Node.js 18+. Он загружает каталог, создаёт заказ с безопасными повторами и принимает вебхуки. Аналог на PHP — после него.

### Node.js

```js
// tessera.js
const BASE = "https://b2b-2jq.pages.dev/v1";
const KEY = process.env.TESSERA_API_KEY; // rk_live_… или rk_test_…

async function call(method, path, { body, idempotencyKey } = {}) {
  const headers = { Authorization: `Bearer ${KEY}`, Accept: "application/json" };
  if (body) headers["Content-Type"] = "application/json";
  if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;

  for (let attempt = 1; ; attempt++) {
    let res;
    try {
      res = await fetch(BASE + path, {
        method, headers, body: body ? JSON.stringify(body) : undefined,
        signal: AbortSignal.timeout(20_000),
      });
    } catch (err) {
      if (attempt < 4) { await sleep(attempt * 1000); continue; } // сеть/таймаут
      throw err;
    }
    const data = await res.json().catch(() => null);
    if (res.ok) return data;

    const code = data?.error?.code ?? `http_${res.status}`;
    const retryable = res.status === 429 || res.status >= 500;
    if (retryable && attempt < 4) {
      const wait = Number(res.headers.get("Retry-After")) || attempt;
      await sleep(wait * 1000);
      continue;
    }
    const e = new Error(code);
    e.code = code; e.status = res.status; e.details = data?.error?.details; e.requestId = data?.error?.request_id;
    throw e;
  }
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

export const listProducts = async () => (await call("GET", "/products")).data;

export const validatePlayer = (productId, playerId) =>
  call("POST", "/players/validate", { body: { product_id: productId, fields: { player_id: playerId } } });

// shopOrderId — номер заказа в вашем магазине; одинаковый при всех повторах
export const createOrder = (shopOrderId, productId, playerId, expectedPrice) =>
  call("POST", "/orders", {
    idempotencyKey: `shop-${shopOrderId}`,
    body: {
      product_id: productId,
      fields: { player_id: playerId },
      client_order_id: String(shopOrderId),
      ...(expectedPrice ? { expected_price: expectedPrice } : {}),
    },
  });

export const getOrder = (number) => call("GET", `/orders/${number}`);
export const getBalance = () => call("GET", "/balance");
```

Использование при оформлении заказа в магазине:

```js
import { createOrder } from "./tessera.js";

try {
  const { order } = await createOrder(58121, "ff.cis.dia.110", "1234567890", "0.95");
  // сохраните order.number у себя; статус придёт вебхуком
} catch (e) {
  if (e.code === "insufficient_balance") { /* уведомите себя: пора пополнить баланс */ }
  else if (e.code === "invalid_player_id") { /* попросите покупателя исправить ID */ }
  else if (e.code === "price_changed") { /* обновите цену на витрине */ }
  else { /* сохраните e.requestId и покажите покупателю «попробуйте позже» */ }
}
```

### PHP

```php
<?php
const TESSERA_BASE = "https://b2b-2jq.pages.dev/v1";

function tessera(string $method, string $path, ?array $body = null, ?string $idemKey = null): array {
  $headers = ["Authorization: Bearer " . getenv("TESSERA_API_KEY"), "Accept: application/json"];
  if ($body !== null) $headers[] = "Content-Type: application/json";
  if ($idemKey !== null) $headers[] = "Idempotency-Key: " . $idemKey;

  for ($attempt = 1; ; $attempt++) {
    $ch = curl_init(TESSERA_BASE . $path);
    curl_setopt_array($ch, [
      CURLOPT_CUSTOMREQUEST => $method,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => $headers,
      CURLOPT_TIMEOUT => 20,
      CURLOPT_POSTFIELDS => $body !== null ? json_encode($body) : null,
    ]);
    $raw = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $retryable = $raw === false || $status === 429 || $status >= 500;
    if ($retryable && $attempt < 4) { sleep($attempt); continue; }
    if ($raw === false) throw new RuntimeException("network_error");

    $data = json_decode($raw, true);
    if ($status >= 400) throw new RuntimeException($data["error"]["code"] ?? "http_$status");
    return $data;
  }
}

$order = tessera("POST", "/orders", [
  "product_id" => "ff.cis.dia.110",
  "fields" => ["player_id" => "1234567890"],
  "client_order_id" => "58121",
], "shop-58121");
echo $order["order"]["number"];
```

---

## 13. Чек-лист перед запуском

- [ ] Ключ хранится только на сервере, в переменной окружения.
- [ ] `Idempotency-Key` выводится из номера заказа магазина и одинаков при всех повторах.
- [ ] При таймауте и 5xx запрос повторяется с тем же ключом.
- [ ] Вебхуки проверяют подпись по сырому телу и возраст события.
- [ ] Повторная доставка вебхука (тот же `X-Event-Id`) не обрабатывается дважды.
- [ ] `failed` у вас означает «деньги вернулись в Tessera» — верните деньги покупателю или предложите повтор.
- [ ] `processing` дольше 10 минут — не возвращайте деньги покупателю сами, дождитесь финального статуса.
- [ ] Вы следите за балансом (`GET /v1/balance`) и пополняете его заранее.
- [ ] В логах сохраняется `request_id` каждой ошибки.
- [ ] Все сценарии из таблицы тестового режима проверены с ключом `rk_test_…`.

---

## 14. Частые вопросы

**Заказ долго в `processing`. Что делать?**
Ждать. Деньги не потеряются: заказ закончится либо `completed`, либо `failed` с автоматическим возвратом. Если больше часа — напишите менеджеру с номером заказа.

**Можно ли отменить заказ?**
Нет. Заказ уходит на выполнение сразу после создания.

**Покупатель ошибся в Player ID и заказ выполнен.**
Пополнение ушло на указанный аккаунт и не может быть отменено. Показывайте покупателю ник через `POST /v1/players/validate` до оплаты.

**Почему товар пропал из каталога?**
Он временно недоступен. Когда станет доступен, вернётся с тем же `product_id`.

**Цена изменилась между показом и заказом.**
Передавайте `expected_price`. При расхождении получите `409 price_changed` с новой ценой, и ничего не будет списано.

**Как сверить баланс?**
`GET /v1/ledger` — каждая операция с остатком после неё.

---

## 15. Заметки для ИИ-ассистентов

Если вы — ИИ-ассистент и пишете интеграцию с Tessera:

- Полная спецификация: `https://b2b-2jq.pages.dev/v1/openapi.json`. Этот файл — человеческое описание поверх неё.
- Базовый адрес: `https://b2b-2jq.pages.dev/v1`. Аутентификация: `Authorization: Bearer rk_live_…` или `rk_test_…`.
- Все суммы — строки, не числа. Не используйте float для денег; сравнивайте строки или переводите в целые микро-единицы (×1 000 000).
- `POST /v1/orders` без `Idempotency-Key` не работает. Ключ должен быть детерминированным от заказа магазина, а не случайным при каждой попытке.
- Не возвращайте покупателю деньги при `processing`. Возврат уместен только при `failed`.
- Подпись вебхука: `HMAC-SHA256(secret, t + "." + raw_body)`, заголовок `X-Signature: t=…,v1=…`.
- Для проверки кода используйте тестовый ключ и Player ID из раздела 4.
- Не храните API-ключ в клиентском коде.

---

## 16. ИИ-коннектор (MCP)

Tessera — MCP-сервер (Model Context Protocol). Его можно подключить к Claude, ChatGPT, Claude Code, Cursor и другим ИИ-ассистентам, и работать с аккаунтом обычным текстом: «покажи баланс», «закинь 110 алмазов на ID 1234567890».

- Адрес: `https://b2b-2jq.pages.dev/mcp`
- Транспорт: Streamable HTTP (`POST`, ответы JSON)
- Вход: OAuth 2.1 с PKCE и динамической регистрацией клиентов, или API-ключ в заголовке `Authorization: Bearer rk_…`

### Подключение

**Claude (claude.ai, приложение):** Настройки → Коннекторы → Добавить свой коннектор → адрес `https://b2b-2jq.pages.dev/mcp` → Подключить. Откроется Tessera: войдите через Google, выберите режим Test или Live и нажмите «Разрешить».

**ChatGPT:** Настройки → Приложения и коннекторы → Дополнительно → режим разработчика → Создать → адрес коннектора, аутентификация OAuth.

**Claude Code:**

```bash
claude mcp add --transport http tessera https://b2b-2jq.pages.dev/mcp
```

Затем `/mcp` → Tessera → вход.

**Cursor, VS Code и другие клиенты с заголовками:**

```json
{
  "mcpServers": {
    "tessera": {
      "url": "https://b2b-2jq.pages.dev/mcp",
      "headers": { "Authorization": "Bearer rk_test_ВАШ_КЛЮЧ" }
    }
  }
}
```

### Инструменты

| Инструмент | Что делает | Метод API |
|---|---|---|
| `list_products` | каталог и цены | `GET /v1/products` |
| `get_product` | один товар | `GET /v1/products/{id}` |
| `validate_player` | ник и регион по Player ID | `POST /v1/players/validate` |
| `create_order` | создаёт заказ, списывает баланс | `POST /v1/orders` |
| `get_order` | статус заказа | `GET /v1/orders/{number}` |
| `list_orders` | список и поиск заказов | `GET /v1/orders` |
| `get_balance` | баланс | `GET /v1/balance` |
| `list_ledger` | выписка | `GET /v1/ledger` |

Инструменты работают через тот же API: те же лимиты, коды ошибок и журнал запросов. Ошибка API возвращается как результат инструмента с `isError: true` и телом ошибки.

### Безопасность

- Каждое подключение через OAuth создаёт отдельный API-ключ «ИИ: <название клиента>». Отозвать доступ — отозвать этот ключ в разделе «API-ключи».
- Режим (Test или Live) выбирается при подключении и задаётся ключом.
- `create_order` в режиме Live тратит реальные деньги. Ассистент получает указание подтверждать каждый заказ у пользователя.

### Технические детали OAuth

| Адрес | Назначение |
|---|---|
| `/.well-known/oauth-protected-resource` | метаданные ресурса (RFC 9728) |
| `/.well-known/oauth-authorization-server` | метаданные сервера авторизации (RFC 8414) |
| `/oauth/register` | динамическая регистрация клиента (RFC 7591) |
| `/connect` | страница авторизации (`response_type=code`, PKCE `S256` обязателен) |
| `/oauth/token` | обмен кода на токен (`grant_type=authorization_code`) |

Код авторизации действует 5 минут. Токен доступа — API-ключ `rk_…`: он не истекает, пока его не отзовут.
