RMT.GG/Документация для разработчиков продавцов
v1

API продавца

Автоматизируйте листинги, выполняйте продажи и отслеживайте события заказов. Включает исходящие вебхуки и конечные точки резервирования для пополнения инвентаря по запросу после оплаты.

REST Open API

Аутентификация Bearer /api/v1 для предложений и заказов с заголовками обнаружения и ограничения скорости.

Исходящие вебхуки

Подписанные HTTPS (или Discord) доставки для событий жизненного цикла заказов и предложений.

Резерв / пополнение

Создавайте COMPLEX запасы с вашего сервера после оплаты, когда местного инвентаря недостаточно.

Что вы можете создать

Открытый API продавца предназначен для продавцов, которые хотят получать уведомления в Discord, синхронизацию запасов, автоматизацию в стиле Zapier или кастомный бэк-офис на базе RMT.GG.

  • Управление предложениями
    Создавайте черновики, обновляйте безопасные поля, публикуйте и архивируйте через /api/v1/offers.
  • Выполнение продаж
    Список и проверка заказов продавца, затем отметьте их как доставленные с необязательными URL-адресами доказательств.
  • Оставайтесь в пределах лимита
    Каждый ключ ограничен 300 запросами в минуту. Ответы включают заголовки X-RateLimit-*.
  • Реагируйте в реальном времени
    Подписывайтесь на события заказов и предложений или пополняйте COMPLEX инвентарь с помощью резервных вебхуков.
  • Принимайте платежи из вашего магазина
    Одобренные партнеры могут отправлять покупателей из внешнего магазина на хостинг-кассу, а затем выполнять заказ после оплаты.

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

Создайте API-ключ в настройках разработчика, затем вызовите discovery, чтобы распечатать живой каталог.

  1. 1Откройте Настройки → Разработчик (отдельного шага включения нет).
  2. 2Создайте API ключ и скопируйте секрет один раз (rmt_sk_live_…). Храните его в своем менеджере секретов.
  3. 3Вызовите GET /api/v1 с Authorization: Bearer, чтобы подтвердить области, квоты и операции.
GET/api/v1

Документ обнаружения

Возвращает области, квоты, события вебхуков и полный каталог операций. Любой действительный API ключ работает.

Аутентификация

Отправляйте свой живой секретный ключ в каждом запросе /api/v1. Предпочитайте только HTTPS. Никогда не встраивайте ключи в публичные клиенты или пакеты браузера.

Предпочтительный заголовок

http
Authorization: Bearer rmt_sk_live_<prefix>_<secret>

Альтернативный заголовок

http
X-Api-Key: rmt_sk_live_<prefix>_<secret>

Повторно используемый клиент TypeScript (Bearer auth, типизированные ошибки, повторная попытка 429)

typescript
const API_BASE = "https://rmt.gg/api/v1";
const API_KEY = process.env.RMT_API_KEY!; // rmt_sk_live_…

export class RmtApiError extends Error {
  constructor(
    readonly status: number,
    readonly code: string | undefined,
    message: string,
    readonly retryAfterSec?: number,
  ) {
    super(message);
    this.name = "RmtApiError";
  }
}

type RmtFetchInit = RequestInit & { idempotencyKey?: string };

export async function rmtFetch<T>(path: string, init: RmtFetchInit = {}): Promise<T> {
  const headers = new Headers(init.headers);
  headers.set("Authorization", `Bearer ${API_KEY}`);
  // Alternate: headers.set("X-Api-Key", API_KEY);
  headers.set("Accept", "application/json");
  if (init.body && !headers.has("Content-Type")) {
    headers.set("Content-Type", "application/json");
  }
  if (init.idempotencyKey) headers.set("Idempotency-Key", init.idempotencyKey);

  const res = await fetch(`${API_BASE}${path}`, { ...init, headers });
  const retryAfter = Number(res.headers.get("Retry-After") ?? "");
  const body = (await res.json().catch(() => ({}))) as {
    error?: string;
    code?: string;
    retryAfter?: number;
  };

  if (res.status === 429) {
    throw new RmtApiError(
      429,
      body.code ?? "RATE_LIMITED",
      body.error ?? "Rate limited",
      Number.isFinite(retryAfter) ? retryAfter : body.retryAfter,
    );
  }
  if (!res.ok) {
    throw new RmtApiError(res.status, body.code, body.error ?? res.statusText);
  }
  return body as T;
}

export async function withRetry<T>(fn: () => Promise<T>, maxAttempts = 4): Promise<T> {
  let attempt = 0;
  for (;;) {
    try {
      return await fn();
    } catch (err) {
      attempt += 1;
      if (!(err instanceof RmtApiError) || err.status !== 429 || attempt >= maxAttempts) {
        throw err;
      }
      const waitSec = Math.max(1, err.retryAfterSec ?? 1);
      await new Promise((r) => setTimeout(r, waitSec * 1000));
    }
  }
}

Смените при утечке

Если ключ утечет, отозвите его в настройках разработчика и создайте новый. Обновите свою автоматизацию перед отзывом, если вы в режиме реального времени.

Области

Каждый API ключ имеет области, которые ограничивают конечные точки. Отсутствующая область возвращает 403 SCOPE_MISSING.

offers:read
offers:write
orders:read
orders:write
webhooks:manage
checkout:write
  • offers:read: Список и получение ваших предложений.
  • offers:write: Создание, обновление, публикация и удаление предложений.
  • orders:read: Список и получение заказов продавца.
  • orders:write: Отметить заказы как доставленные.
  • webhooks:manage: Резервировано для будущего управления вебхуками Open API. Настройте Discord/Telegram в Уведомлениях и JSON вебхуки в настройках разработчика уже сегодня.
  • checkout:write: Создание и чтение сессий хостинг-кассы. Требуется одобрение администратора для партнерской кассы.

Общие области ключей

Новые ключи получают offers:read, offers:write, orders:read и orders:write. CRUD для исходящих вебхуков остается в интерфейсе настроек (аутентификация сессии).

API предложений

Идентификаторы предложений принимают публичный URL-слуг или числовой ID. Ответы не содержат внутренний ID и sellerId.

Что PATCH пока не может изменить

Строки запасов, цены опций, медиа и атрибуты управляются в редакторе продавца (или будущих конечных точках), а не через PATCH сегодня.

GET/api/v1/offers
offers:read

Список ваших предложений

Фильтруйте с archive=active (по умолчанию), archived или all.

Запрос

  • archive
    В
    query
    Тип
    string
    Обязательно
    Необязательно
    Описание
    One of "active" (default), "archived", or "all".
  • Response: { offers: Offer[], total: number }. Numeric id and sellerId are omitted.
POST/api/v1/offers
offers:write

Создать черновик предложения

Создает пустой черновик, принадлежащий аутентифицированному продавцу. Тело не требуется.

  • No request body required.
  • Response 201: { offer: Offer }.
GET/api/v1/offers/:urlOrId
offers:read

Получить одно предложение

Загрузите по публичному URL-слугу или числовому ID. Связи (опции) могут быть включены; запасные предметы не включены.

Запрос

  • urlOrId
    В
    path
    Тип
    string
    Обязательно
    обязательно
    Описание
    Offer.url slug or Offer.id.
  • Returns relations (options, etc.) when available; items are not included.
PATCH/api/v1/offers/:urlOrId
offers:write

Обновить поля предложения

Обновите безопасный подмножество полей листинга. Вызывает offer.updated, когда настроены исходящие вебхуки.

Запрос

  • urlOrId
    В
    path
    Тип
    string
    Обязательно
    обязательно
    Описание
    Offer.url slug or Offer.id.
  • title
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Описание
    Listing title.
  • description
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Описание
    Listing description.
  • visibility
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Описание
    PUBLIC | PRIVATE | UNPUBLISHED.
  • categoryId
    В
    body
    Тип
    number
    Обязательно
    Необязательно
    Описание
    Catalog category id.
  • offeringId
    В
    body
    Тип
    number
    Обязательно
    Необязательно
    Описание
    Catalog offering id.
  • thumbnail
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Описание
    Thumbnail URL or asset reference.
  • offerType
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Описание
    Offer type string used by the listing.
  • listingMode
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Описание
    Listing mode (for example STANDARD, RANK_BOOST, SESSION).
  • At least one allowed field is required.
  • Emits offer.updated webhook when configured.
  • Stock is managed via GET/POST /api/v1/offers/:urlOrId/stock. Option prices, media, and attributes are not editable via this endpoint yet.
DELETE/api/v1/offers/:urlOrId
offers:write

Удалить или архивировать

Те же правила удаления/архивирования, что и в интерфейсе продавца.

Запрос

  • urlOrId
    В
    path
    Тип
    string
    Обязательно
    обязательно
    Описание
    Offer.url slug or Offer.id.
  • Response: { ok: true }.
POST/api/v1/offers/:urlOrId/publish
offers:write

Опубликовать предложение

Публикует черновик (или изменяет видимость). Не удается с 400, если обязательные поля листинга неполные.

Запрос

  • urlOrId
    В
    path
    Тип
    string
    Обязательно
    обязательно
    Описание
    Offer.url slug or Offer.id.
  • visibility
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Описание
    Optional. PUBLIC (default), PRIVATE, or UNPUBLISHED.
  • Response: { offer: Offer }.
  • Fails if the listing is incomplete for publish.

API Запасов

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

Как работает сопоставление

GET /api/v1/stock?fields=username,password находит предложения, схема которых содержит эти поля. Пополните с названиями опций (или optionId) и названиями полей. Вам не нужны внутренние идентификаторы полей. Ответы никогда не содержат значения учетных данных.

GET/api/v1/stock
offers:read

Список запасов по вашим предложениям

Возвращает количество по уровням и названия полей доставки, чтобы вы могли сопоставить ключи и аккаунты с правильным предложением. Фильтруйте с помощью q, fields, stockMode и lowStock. Никогда не возвращает значения учетных данных.

Запрос

  • q
    В
    query
    Тип
    string
    Обязательно
    Необязательно
    Лимиты
    Max 80
    Описание
    Filter by listing title or url slug.
  • fields
    В
    query
    Тип
    string
    Обязательно
    Необязательно
    Описание
    Comma-separated delivery field names. The listing must have all of them (Username,Password). Names match case-insensitively.
  • stockMode
    В
    query
    Тип
    string
    Обязательно
    Необязательно
    Описание
    QUANTITY or COMPLEX. Listing must have at least one option in that mode.
  • lowStock
    В
    query
    Тип
    number
    Обязательно
    Необязательно
    Описание
    Keep listings that have a finite tier with available less than or equal to this number.
  • archive
    В
    query
    Тип
    string
    Обязательно
    Необязательно
    Описание
    One of "active" (default), "archived", or "all".
  • Response: { offers: StockOffer[], total: number }. Numeric offer id is omitted. Option id is included so you can restock a specific tier.
  • available is the buyable count. null with unlimited true means unlimited quantity or on-demand COMPLEX inventory.
  • fields[] is the listing delivery schema (empty for quantity-only listings). Use it to map keys and accounts without field ids.
  • This endpoint never returns credential values.
GET/api/v1/offers/:urlOrId/stock
offers:read

Получить запасы для одного предложения

Та же форма StockOffer, что и в индексе, для одного url или числового id. Только количество.

Запрос

  • urlOrId
    В
    path
    Тип
    string
    Обязательно
    обязательно
    Описание
    Offer.url slug or Offer.id.
  • Response: { offer } with the same StockOffer shape as GET /api/v1/stock.
  • Counts only. Use the seller editor to inspect saved key values.
POST/api/v1/offers/:urlOrId/stock
offers:write

Пополнить предложение

Уровни количества: добавить, удалить или установить. Уровни сохраненных предметов: объекты предметов по названию поля, keys[] когда есть одно поле, или текст с разделителями. Несколько уровней в одном вызове через options[]. dryRun предварительно показывает сопоставление. onDuplicate по умолчанию пропускает.

json
{
  "option": "1 Month",
  "add": 50
}

Запрос

  • urlOrId
    В
    path
    Тип
    string
    Обязательно
    обязательно
    Описание
    Offer.url slug or Offer.id.
  • option
    В
    body
    Тип
    string
    Обязательно
    Условный
    Описание
    Pricing option name (case-insensitive). Omit when the listing has a single tier.
  • optionId
    В
    body
    Тип
    number
    Обязательно
    Условный
    Описание
    Pricing option id from GET stock. Wins over option when both are sent. Ambiguous names return 409 OPTION_AMBIGUOUS.
  • add
    В
    body
    Тип
    number
    Обязательно
    Условный
    Лимиты
    1-1,000,000
    Описание
    QUANTITY: add this many units. Fails with 400 UNLIMITED_STOCK if the tier is unlimited.
  • remove
    В
    body
    Тип
    number
    Обязательно
    Условный
    Лимиты
    1-1,000,000
    Описание
    QUANTITY: withdraw this many units. Fails with 400 INSUFFICIENT_STOCK when there is not enough.
  • set
    В
    body
    Тип
    number | null
    Обязательно
    Условный
    Описание
    QUANTITY: set an absolute count. null means unlimited. Cannot go below units held in checkout.
  • items
    В
    body
    Тип
    object[]
    Обязательно
    Условный
    Лимиты
    Max 1,000
    Описание
    COMPLEX: objects keyed by delivery field name, for example { "Username": "a", "Password": "b" }. Names match case-insensitively.
  • keys
    В
    body
    Тип
    string[]
    Обязательно
    Условный
    Лимиты
    Max 1,000
    Описание
    COMPLEX: license keys when the listing has exactly one delivery field. Otherwise 400 FIELD_MAPPING_AMBIGUOUS.
  • text
    В
    body
    Тип
    string
    Обязательно
    Условный
    Лимиты
    Max 1,000 rows
    Описание
    COMPLEX: delimited paste. A header row that matches field names is detected automatically. Otherwise columns map in field sort order when the column count matches.
  • delimiter
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Лимиты
    Default :
    Описание
    Delimiter for text. Ignored unless text is sent.
  • headers
    В
    body
    Тип
    string[]
    Обязательно
    Необязательно
    Описание
    Optional column headers for text when the first line is data, not names.
  • options
    В
    body
    Тип
    object[]
    Обязательно
    Условный
    Описание
    Restock several tiers in one call. Each element is the same shape as a single-option body (option, add, items, …).
  • dryRun
    В
    body
    Тип
    boolean
    Обязательно
    Необязательно
    Описание
    Preview matching and counts without writing. Default false.
  • onDuplicate
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Лимиты
    skip (default) or error
    Описание
    COMPLEX: skip existing unsold fingerprints, or fail the request with 409 DUPLICATE_ITEMS.
  • Send exactly one action per option: add, remove, set, items, keys, or text.
  • Sending items to a QUANTITY tier (or add to a COMPLEX tier) returns 400 STOCK_MODE_MISMATCH.
  • Responses never echo credential values. COMPLEX results include imported, skippedDuplicates, errors, and matchedFields.
  • Saved items are capped at 5,000 unsold rows per option. A single request may import at most 1,000 rows.

Импорт множества аккаунтов или ключей

Гид

Используйте POST /api/v1/offers/:url/stock с items[] для аккаунтов или keys[] для лицензий с одним полем. Разбивайте на 1,000 строк за запрос.

  1. 1ПОЛУЧИТЕ объявление. Используйте fields[] и stockMode для выбора предметов, ключей или добавления.
  2. 2Сохраните аккаунты в формате JSON или CSV, используя имена полей в качестве ключей. Сохраняйте лицензионные ключи по одному на строку.
  3. 3Сначала выполните пробный запуск. Проверьте wouldImport, skippedDuplicates и matchedFields.
  4. 4Сделайте POST с тем же телом снова без dryRun, чтобы записать запасы.

Выберите полезную нагрузку, соответствующую объявлению

Сначала выполните GET stock. Если fields[] содержит более одного имени, отправьте объекты items с ключами по этим именам (Имя пользователя, Пароль, Электронная почта). Если есть ровно одно поле, достаточно keys[]. Для количественных объявлений используйте add, а не items.

1,000 строк на запрос. 5,000 непроданных предметов на уровень. 300 запросов в минуту. Дубликаты пропускаются по умолчанию.

accounts.json (по одному объекту на аккаунт)

json
[
  { "Username": "player1", "Password": "secret1", "E-Mail": "[email protected]" },
  { "Username": "player2", "Password": "secret2", "E-Mail": "[email protected]" }
]

accounts.csv

csv
Username,Password,E-Mail
player1,secret1,p1@example.com
player2,secret2,p2@example.com

keys.txt (по одному лицензионному ключу на строку)

text
AAAA-BBBB-CCCC
DDDD-EEEE-FFFF
GGGG-HHHH-IIII

cURL

bash
# Inspect field names and stockMode
curl -s -H "Authorization: Bearer rmt_sk_live_…" \
  -H "Accept: application/json" \
  https://rmt.gg/api/v1/offers/YOUR_OFFER_URL/stock

# Preview (no write)
curl -s -X POST \
  -H "Authorization: Bearer rmt_sk_live_…" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  https://rmt.gg/api/v1/offers/YOUR_OFFER_URL/stock \
  -d '{"dryRun":true,"onDuplicate":"skip","option":"Premium","items":[{"Username":"player1","Password":"secret1","E-Mail":"[email protected]"}]}'

# Apply accounts
curl -s -X POST \
  -H "Authorization: Bearer rmt_sk_live_…" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  https://rmt.gg/api/v1/offers/YOUR_OFFER_URL/stock \
  -d '{"onDuplicate":"skip","option":"Premium","items":[{"Username":"player1","Password":"secret1","E-Mail":"[email protected]"}]}'

# Apply license keys (listing must have exactly one delivery field)
curl -s -X POST \
  -H "Authorization: Bearer rmt_sk_live_…" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  https://rmt.gg/api/v1/offers/YOUR_OFFER_URL/stock \
  -d '{"option":"Steam","keys":["AAAA-BBBB-CCCC","DDDD-EEEE-FFFF"]}'

# Or paste CSV / colon-separated rows in text
curl -s -X POST \
  -H "Authorization: Bearer rmt_sk_live_…" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  https://rmt.gg/api/v1/offers/YOUR_OFFER_URL/stock \
  -d '{"option":"Premium","delimiter":",","text":"Username,Password,E-Mail\nplayer1,secret1,[email protected]"}'

Пакетный импорт TypeScript (1,000 строк за запрос)

typescript
// Paste rmtFetch and withRetry from the Auth section first.
const CHUNK = 1000;

async function importRows(offerUrl: string, option: string, rows: Array<Record<string, string>>) {
  for (let i = 0; i < rows.length; i += CHUNK) {
    const items = rows.slice(i, i + CHUNK);
    const preview = await rmtFetch<{
      results: Array<{ wouldImport: number; skippedDuplicates: number; errors: string[] }>;
    }>(`/offers/${offerUrl}/stock`, {
      method: "POST",
      body: JSON.stringify({ dryRun: true, onDuplicate: "skip", option, items }),
    });
    const row = preview.results[0];
    if ((row?.errors?.length ?? 0) > 0) {
      throw new Error(row.errors.join("; "));
    }
    await withRetry(() =>
      rmtFetch(`/offers/${offerUrl}/stock`, {
        method: "POST",
        body: JSON.stringify({ onDuplicate: "skip", option, items }),
      }),
    );
  }
}

// License keys: only when GET stock.fields has exactly one name
async function importKeys(offerUrl: string, option: string, keys: string[]) {
  for (let i = 0; i < keys.length; i += CHUNK) {
    await withRetry(() =>
      rmtFetch(`/offers/${offerUrl}/stock`, {
        method: "POST",
        body: JSON.stringify({ option, keys: keys.slice(i, i + CHUNK) }),
      }),
    );
  }
}

Что защищает этот API

Ключи требуют offers:write, имеют ограничения по частоте и могут пополнять только ваши собственные объявления. GET никогда не возвращает сохраненные учетные данные. Ответы на POST не отображают значения Имя пользователя, Пароль или ключи. Отправляйте тело через HTTPS в продакшене и храните API-ключ в переменной окружения.

На Windows используйте curl.exe (не алиас curl). Оберните -d JSON в кавычки, чтобы PowerShell не разбивал его.

API заказов

Заказы ограничены вашим аккаунтом продавца. Платежные данные покупателя могут быть скрыты в соответствии с правилами конфиденциальности рынка.

GET/api/v1/orders
orders:read

Список заказов продавца

Поддерживает limit, offset, status, q и sort (новейшие, старейшие, total_high, total_low).

Запрос

  • limit
    В
    query
    Тип
    number
    Обязательно
    Необязательно
    Лимиты
    1-100, default 20
    Описание
    Page size.
  • offset
    В
    query
    Тип
    number
    Обязательно
    Необязательно
    Лимиты
    >= 0, default 0
    Описание
    Skip this many rows.
  • status
    В
    query
    Тип
    string
    Обязательно
    Необязательно
    Лимиты
    Max 32
    Описание
    Filter by order status (for example PAID, DELIVERED, COMPLETED).
  • q
    В
    query
    Тип
    string
    Обязательно
    Необязательно
    Лимиты
    Max 80
    Описание
    Search reference or related text.
  • sort
    В
    query
    Тип
    string
    Обязательно
    Необязательно
    Лимиты
    newest (default)
    Описание
    newest | oldest | total_high | total_low.
  • Response: { orders: Order[], total: number }.
GET/api/v1/orders/:uid
orders:read

Получить один заказ

Возвращает заказ с позициями. Используйте публичный uid заказа.

Запрос

  • uid
    В
    path
    Тип
    string
    Обязательно
    обязательно
    Описание
    Order.uid.
  • Response: { order } with line items.
  • Buyer billing fields may be redacted under marketplace-of-record privacy rules.
POST/api/v1/orders/:uid/deliver
orders:write

Отметить как доставленный

Ручное выполнение. COMPLEX строки должны быть полностью прикреплены, когда это требуется. Вызывает order.delivered.

Запрос

  • uid
    В
    path
    Тип
    string
    Обязательно
    обязательно
    Описание
    Order.uid.
  • evidence
    В
    body
    Тип
    string[]
    Обязательно
    Необязательно
    Лимиты
    HTTPS, max 10
    Описание
    Optional screenshot or transfer-proof URLs.
  • Response: { success: true, order }.
  • COMPLEX inventory lines must be fully attached before deliver when the product requires it.
  • Emits order.delivered webhook when configured.

Хостинг-касса

Любой одобренный партнерский магазин или бэкенд могут отправить покупателей на страницу оплаты RMT.GG. Мы остаемся продавцом и берем 4% от заблокированной суммы.

Разрешенные и выполнение

Перейдите в Настройки, Хостинговый чек-аут, затем создайте API-ключ и JSON вебхук. После оплаты мы отправляем checkout.completed. Значения доставки остаются в подтверждении RMT.GG; их нет в GET продавца или вебхуках.

POST/api/v1/checkout/sessions
checkout:write

Создать сессию хостинг-кассы

Перенаправьте покупателей на заблокированную страницу оплаты RMT.GG. Один предмет: сумма и itemName. Корзина: items[] с названием и суммой в каждой строке. Валюта по умолчанию - USD. После оплаты покупатель остается на RMT.GG, когда есть поля для доставки, которые нужно скопировать. returnUrl ведет обратно в магазин; при отсутствии доставки мы возвращаем их после короткого обратного отсчета. Суммы, длины и другие ограничения указаны в колонке Limits.

javascript
{
  amount:   10,           // what the buyer pays
  itemName: "Gold pack",  // pay page heading
}

Запрос

  • amount
    В
    body
    Тип
    number
    Обязательно
    Условный
    Лимиты
    > 0, max 1,000,000
    Описание
    What the buyer pays. Required for a single item. With items[], omit it or send the line sum. Mismatch: 400 AMOUNT_MISMATCH.
  • currency
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Лимиты
    Default USD
    Описание
    ISO 4217 code such as USD or EUR.
  • itemName
    В
    body
    Тип
    string
    Обязательно
    Условный
    Лимиты
    Max 120
    Описание
    Pay page heading. Required for a single item. Alias: title. With items[], defaults to the first line name. Missing: 400 ITEM_NAME_REQUIRED.
  • title
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Описание
    Alias of itemName. If both are sent, itemName wins.
  • description
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Лимиты
    Max 200
    Описание
    Copy under the heading. If omitted, the heading is reused.
  • imageUrl
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Лимиты
    HTTPS, max 2048
    Описание
    Product image, or fallback for lines without imageUrl. Invalid: 400 INVALID_IMAGE_URL.
  • items
    В
    body
    Тип
    object[]
    Обязательно
    Условный
    Лимиты
    1-20 lines, JSON max 48,000
    Описание
    Locked cart. Required when amount is omitted. Buyers cannot change lines. Empty: 400 INVALID_ITEMS.
  • items[].name
    В
    body
    Тип
    string
    Обязательно
    обязательно
    Лимиты
    Max 120
    Описание
    Line title. Alias: title.
  • items[].title
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Описание
    Alias of items[].name. If both are sent, name wins.
  • items[].description
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Лимиты
    Max 200
    Описание
    Line copy under the name.
  • items[].amount
    В
    body
    Тип
    number
    Обязательно
    обязательно
    Лимиты
    > 0, max 1,000,000
    Описание
    Unit price. Session total is sum(amount * quantity).
  • items[].quantity
    В
    body
    Тип
    number
    Обязательно
    Необязательно
    Лимиты
    1-99, default 1
    Описание
    Locked on the pay page.
  • items[].imageUrl
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Лимиты
    HTTPS, max 2048
    Описание
    Line image. Falls back to top-level imageUrl.
  • items[].delivery
    В
    body
    Тип
    object[]
    Обязательно
    Необязательно
    Лимиты
    Max 16 fields
    Описание
    Shown after payment on RMT.GG. Seller GET and webhooks omit values.
  • items[].delivery[].name
    В
    body
    Тип
    string
    Обязательно
    обязательно
    Лимиты
    Max 80
    Описание
    Field label, for example Code or Password.
  • items[].delivery[].type
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Лимиты
    text, password, textarea
    Описание
    password is blurred until the buyer reveals it. Default text.
  • items[].delivery[].value
    В
    body
    Тип
    string
    Обязательно
    обязательно
    Лимиты
    Max 2048
    Описание
    Field value. Numbers are stored as strings. Empty: 400 INVALID_DELIVERY.
  • email
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Лимиты
    Invalid values ignored
    Описание
    Prefills the pay page. The buyer still confirms email before paying.
  • returnUrl
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Лимиты
    HTTPS, max 2048
    Описание
    Continue-to-shop after payment. Delivery fields keep the buyer on RMT.GG with a button. No delivery: we send them back after a short countdown. If omitted, there is no shop button. http://localhost is allowed for local shops.
  • cancelUrl
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Лимиты
    HTTPS, max 2048
    Описание
    Redirect if the buyer cancels or the session expires. If omitted, they stay on the pay page.
  • invoiceId
    В
    body
    Тип
    string
    Обязательно
    Необязательно
    Лимиты
    Max 128
    Описание
    Your shop id. Same payload returns the existing session. A different payload: 409 INVOICE_CONFLICT.
  • categorySlug
    В
    body
    Тип
    string
    Обязательно
    Условный
    Лимиты
    With offering, or omit both
    Описание
    Public root slug such as games. Used for card, PayPal, and crypto labels, not the pay page title. Wrong pair: 400 INVALID_PSP_CATEGORY.
  • offering
    В
    body
    Тип
    string
    Обязательно
    Условный
    Лимиты
    With categorySlug, or omit both
    Описание
    Catalog offering such as Mods. Mapped to labels like Games · Add-ons.
  • metadata
    В
    body
    Тип
    object
    Обязательно
    Необязательно
    Лимиты
    Object, max 4096 chars
    Описание
    Stored on the session. Not returned on seller GET.
  • Idempotency-Key
    В
    header
    Тип
    string
    Обязательно
    Необязательно
    Лимиты
    Max 128
    Описание
    Replay header. Same key and payload returns the existing session. A different payload: 409 IDEMPOTENCY_CONFLICT.

Ответ

  • uid
    В
    response
    Тип
    string
    Описание
    Session id. Same value as in hostedUrl / hosted_url.
  • status
    В
    response
    Тип
    string
    Описание
    created | pending_payment | paid | canceled | expired | refunded | error. Unpaid sessions expire after 24 hours.
  • paid
    В
    response
    Тип
    boolean
    Описание
    true only when status is paid. false for refunded, expired, canceled, and unpaid states.
  • amount
    В
    response
    Тип
    number
    Описание
    Locked buyer total in major units.
  • currency
    В
    response
    Тип
    string
    Описание
    ISO currency code stored on the session (for example USD).
  • itemName
    В
    response
    Тип
    string | null
    Описание
    Pay page heading.
  • description
    В
    response
    Тип
    string
    Описание
    Longer copy under the heading.
  • email
    В
    response
    Тип
    string | null
    Описание
    Prefill or confirmed buyer email. Guest checkout placeholders are returned as null.
  • lang
    В
    response
    Тип
    string | null
    Описание
    Buyer locale when known. Not a create-session field.
  • returnUrl
    В
    response
    Тип
    string | null
    Описание
    Continue-to-shop URL stored on the session, or null.
  • cancelUrl
    В
    response
    Тип
    string | null
    Описание
    Cancel/expiry redirect, or null.
  • invoiceId
    В
    response
    Тип
    string | null
    Описание
    Your invoice id. Same value as externalInvoiceId.
  • externalInvoiceId
    В
    response
    Тип
    string | null
    Описание
    Same as invoiceId (legacy alias).
  • source
    В
    response
    Тип
    string
    Описание
    How the session was created. API sessions are "api".
  • expiresAt
    В
    response
    Тип
    string
    Описание
    ISO timestamp. Unpaid checkouts cannot be completed after this time.
  • hostedUrl
    В
    response
    Тип
    string
    Описание
    Pay page URL (same target as top-level hosted_url on create).
  • orderUid
    В
    response
    Тип
    string | null
    Описание
    Marketplace order uid after payment. null until the session is paid.
  • items
    В
    response
    Тип
    object[]
    Описание
    Locked lines: name, description, amount, quantity, imageUrl. Seller GET never includes delivery values.
  • hosted_url
    В
    response
    Тип
    string
    Описание
    Pay page URL. Send the buyer here. Same target as hostedUrl.
  • expires_at
    В
    response
    Тип
    string
    Описание
    ISO timestamp. Same value as expiresAt.
  • Approved partners only. Platform fee is 4% of the locked amount. Payment-method costs are absorbed by the platform.
  • After payment the buyer stays on RMT.GG so they can copy delivery fields. returnUrl is a continue button when delivery is present. With no delivery fields we send them back after a short countdown. If you omit them, they stay on the pay page after payment, cancel, or expiry.
  • Fulfill on checkout.completed. Sessions expire after 24 hours. Buyers cannot change line items. imageUrl must be HTTPS. categorySlug and offering must be sent together (or omit both); a wrong pair returns 400 INVALID_PSP_CATEGORY.
GET/api/v1/checkout/sessions/:uid
checkout:write

Получить сессию хостинг-кассы

Возвращает сессию, которую вы создали. Используйте это, если checkout.completed задерживается. paid истинно только когда статус оплачен. items никогда не включают значения доставки.

Запрос

  • uid
    В
    path
    Тип
    string
    Обязательно
    обязательно
    Описание
    Session uid returned at create time.

Ответ

  • uid
    В
    response
    Тип
    string
    Описание
    Session id. Same value as in hostedUrl / hosted_url.
  • status
    В
    response
    Тип
    string
    Описание
    created | pending_payment | paid | canceled | expired | refunded | error. Unpaid sessions expire after 24 hours.
  • paid
    В
    response
    Тип
    boolean
    Описание
    true only when status is paid. false for refunded, expired, canceled, and unpaid states.
  • amount
    В
    response
    Тип
    number
    Описание
    Locked buyer total in major units.
  • currency
    В
    response
    Тип
    string
    Описание
    ISO currency code stored on the session (for example USD).
  • itemName
    В
    response
    Тип
    string | null
    Описание
    Pay page heading.
  • description
    В
    response
    Тип
    string
    Описание
    Longer copy under the heading.
  • email
    В
    response
    Тип
    string | null
    Описание
    Prefill or confirmed buyer email. Guest checkout placeholders are returned as null.
  • lang
    В
    response
    Тип
    string | null
    Описание
    Buyer locale when known. Not a create-session field.
  • returnUrl
    В
    response
    Тип
    string | null
    Описание
    Continue-to-shop URL stored on the session, or null.
  • cancelUrl
    В
    response
    Тип
    string | null
    Описание
    Cancel/expiry redirect, or null.
  • invoiceId
    В
    response
    Тип
    string | null
    Описание
    Your invoice id. Same value as externalInvoiceId.
  • externalInvoiceId
    В
    response
    Тип
    string | null
    Описание
    Same as invoiceId (legacy alias).
  • source
    В
    response
    Тип
    string
    Описание
    How the session was created. API sessions are "api".
  • expiresAt
    В
    response
    Тип
    string
    Описание
    ISO timestamp. Unpaid checkouts cannot be completed after this time.
  • hostedUrl
    В
    response
    Тип
    string
    Описание
    Pay page URL (same target as top-level hosted_url on create).
  • orderUid
    В
    response
    Тип
    string | null
    Описание
    Marketplace order uid after payment. null until the session is paid.
  • items
    В
    response
    Тип
    object[]
    Описание
    Locked lines: name, description, amount, quantity, imageUrl. Seller GET never includes delivery values.
  • Response: { session }. Stale unpaid sessions are marked expired before they are returned.
  • Use this as a backup to checkout.completed. paid is true only when status is paid.
  • 404 NOT_FOUND if the uid is unknown or belongs to another seller.
GET/api/v1/checkout/sessions
checkout:write

Поиск хостинг-сессии оформления заказа по ID счета

Тот же объект сессии, что и GET по uid. Передайте invoiceId, который вы отправили при создании. Отсутствует: 400 INVOICE_ID_REQUIRED. Неизвестно: 404 NOT_FOUND.

Запрос

  • invoiceId
    В
    query
    Тип
    string
    Обязательно
    обязательно
    Лимиты
    Max 128
    Описание
    invoiceId from create. Missing: 400 INVOICE_ID_REQUIRED. Too long: 400 INVALID_INVOICE_ID.

Ответ

  • uid
    В
    response
    Тип
    string
    Описание
    Session id. Same value as in hostedUrl / hosted_url.
  • status
    В
    response
    Тип
    string
    Описание
    created | pending_payment | paid | canceled | expired | refunded | error. Unpaid sessions expire after 24 hours.
  • paid
    В
    response
    Тип
    boolean
    Описание
    true only when status is paid. false for refunded, expired, canceled, and unpaid states.
  • amount
    В
    response
    Тип
    number
    Описание
    Locked buyer total in major units.
  • currency
    В
    response
    Тип
    string
    Описание
    ISO currency code stored on the session (for example USD).
  • itemName
    В
    response
    Тип
    string | null
    Описание
    Pay page heading.
  • description
    В
    response
    Тип
    string
    Описание
    Longer copy under the heading.
  • email
    В
    response
    Тип
    string | null
    Описание
    Prefill or confirmed buyer email. Guest checkout placeholders are returned as null.
  • lang
    В
    response
    Тип
    string | null
    Описание
    Buyer locale when known. Not a create-session field.
  • returnUrl
    В
    response
    Тип
    string | null
    Описание
    Continue-to-shop URL stored on the session, or null.
  • cancelUrl
    В
    response
    Тип
    string | null
    Описание
    Cancel/expiry redirect, or null.
  • invoiceId
    В
    response
    Тип
    string | null
    Описание
    Your invoice id. Same value as externalInvoiceId.
  • externalInvoiceId
    В
    response
    Тип
    string | null
    Описание
    Same as invoiceId (legacy alias).
  • source
    В
    response
    Тип
    string
    Описание
    How the session was created. API sessions are "api".
  • expiresAt
    В
    response
    Тип
    string
    Описание
    ISO timestamp. Unpaid checkouts cannot be completed after this time.
  • hostedUrl
    В
    response
    Тип
    string
    Описание
    Pay page URL (same target as top-level hosted_url on create).
  • orderUid
    В
    response
    Тип
    string | null
    Описание
    Marketplace order uid after payment. null until the session is paid.
  • items
    В
    response
    Тип
    object[]
    Описание
    Locked lines: name, description, amount, quantity, imageUrl. Seller GET never includes delivery values.
  • Same { session } body as GET /api/v1/checkout/sessions/:uid, including the fields above.
  • Prefer this when you stored your own invoice id and not the session uid.
  • 404 NOT_FOUND if no session exists for that invoice id.

Исходящие вебхуки

Настройте HTTPS конечные точки (или вебхуки Discord) в Настройки → Разработчик. RMT отправляет POST, когда срабатывают подписанные события.

order.paid
order.delivered
order.completed
order.refunded
order.disputed
offer.published
offer.updated
checkout.completed
checkout.canceled
checkout.refunded
  • Формат JSON отправляет структурированный конверт с id, типом, созданием и данными.
  • Формат Discord отправляет богатые вложения с ссылками на заказы или предложения.
  • Необязательная подпись использует X-RMT-Timestamp и X-RMT-Signature (та же схема, что и резерв).
  • История доставки отображается под каждой конечной точкой, чтобы вы могли повторно попытаться выполнить неудачи. Конечные точки автоматически приостанавливаются после повторных неудач.
  • Хостинговый чек-аут отправляет checkout.completed, checkout.canceled и checkout.refunded с данными.checkout. Значения доставки опускаются. Продажи на маркетплейсе сохраняют order.paid и другие события order.*.

Конверт доставки JSON

json
{
  "id": "whd_…",
  "type": "order.paid",
  "created": "2026-07-23T12:00:00.000Z",
  "data": {
    "order": {
      "uid": "ord_…",
      "reference": "RMT-…",
      "status": "PAID",
      "url": "https://rmt.gg/orders/ord_…",
      "items": [ /* line items with offer names */ ]
    }
  }
}

Подписанные заголовки доставки

json
{
  "X-RMT-Event": "order.paid",
  "X-RMT-Delivery": "whd_…",
  "X-RMT-Timestamp": "1710000000",
  "X-RMT-Signature": "v1=abc123…"
}

payload завершения покупки

json
{
  "id": "whd_…",
  "type": "checkout.completed",
  "created": "2026-08-14T12:00:00.000Z",
  "data": {
    "checkout": {
      "uid": "pcs_…",
      "status": "paid",
      "amount": 10,
      "currency": "USD",
      "itemName": "Gold pack",
      "description": "1000 gold for account example",
      "invoiceId": "inv-12345",
      "source": "api",
      "orderUid": "ord_…",
      "hostedUrl": "https://rmt.gg/pay/pcs_…",
      "email": "[email protected]",
      "paidAt": "2026-08-14T12:01:00.000Z",
      "expiresAt": "2026-08-15T12:00:00.000Z",
      "createdAt": "2026-08-14T12:00:00.000Z",
      "reason": null,
      "items": [
        {
          "name": "Gold pack",
          "description": "1000 gold for account example",
          "amount": 10,
          "quantity": 1,
          "imageUrl": "https://cdn.shop.example/gold.png"
        }
      ]
    }
  }
}

payload checkout.canceled

json
{
  "id": "whd_…",
  "type": "checkout.canceled",
  "created": "2026-08-14T12:20:00.000Z",
  "data": {
    "checkout": {
      "uid": "pcs_…",
      "status": "canceled",
      "amount": 10,
      "currency": "USD",
      "itemName": "Gold pack",
      "description": "1000 gold for account example",
      "invoiceId": "inv-12345",
      "source": "api",
      "orderUid": null,
      "hostedUrl": "https://rmt.gg/pay/pcs_…",
      "email": "[email protected]",
      "paidAt": null,
      "expiresAt": "2026-08-15T12:00:00.000Z",
      "createdAt": "2026-08-14T12:00:00.000Z",
      "reason": "buyer_canceled",
      "items": [
        {
          "name": "Gold pack",
          "description": "1000 gold for account example",
          "amount": 10,
          "quantity": 1,
          "imageUrl": "https://cdn.shop.example/gold.png"
        }
      ]
    }
  }
}

Проверка подписей вебхуков

Когда установлена секретная подпись, вычислите HMAC-SHA256 по timestamp + '.' + rawBody и сравните с шестнадцатеричным значением после v1=.

Секрет остается на RMT. Каждый подписанный POST включает X-RMT-Timestamp (Unix секунды) и X-RMT-Signature (v1= плюс hex). Вычислите HMAC-SHA256 для строки timestamp + '.' + rawBody, используя ваш секрет, затем сравните с hex после v1=. Отклоняйте временные метки старше 5 минут.

  • Читать сырые байты тела точно так, как они получены. Не парсить JSON и не сериализовать заново перед хешированием.
  • Используйте значение заголовка X-RMT-Timestamp в качестве префикса временной метки (та же строка, без переработки).
  • Сравните с безопасной проверкой на равенство по времени. Отклоняйте запросы с отсутствующими или несовпадающими подписями, когда настроен секрет.
  • Отклоняйте временные метки старше 5 минут, чтобы ограничить повторные передачи. Та же схема применяется к reserve.item и событиям исходящего заказа или оформления.

Проверка TypeScript (безопасное сравнение по времени и окно повторной передачи 5 минут)

typescript
import { createHmac, timingSafeEqual } from "node:crypto";

const MAX_AGE_SEC = 5 * 60; // reject replays older than 5 minutes

export function verifyRmtSignature(opts: {
  secret: string;
  timestamp: string | null | undefined;
  signatureHeader: string | null | undefined;
  rawBody: string; // exact POST bytes. Do not JSON.parse then re-stringify.
  nowSec?: number;
}): boolean {
  const secret = opts.secret.trim();
  const timestamp = String(opts.timestamp ?? "").trim();
  const provided = String(opts.signatureHeader ?? "").trim().replace(/^v1=/i, "");
  if (!secret || !timestamp || !provided) return false;

  const ts = Number(timestamp);
  if (!Number.isInteger(ts) || ts <= 0) return false;
  const nowSec = opts.nowSec ?? Math.floor(Date.now() / 1000);
  if (Math.abs(nowSec - ts) > MAX_AGE_SEC) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${opts.rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(provided.toLowerCase(), "utf8");
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

// Express / Node HTTP example:
// const rawBody = (req as { rawBody?: string }).rawBody
//   ?? JSON.stringify(req.body); // only if you captured the raw string first
// const ok = verifyRmtSignature({
//   secret: process.env.RMT_WEBHOOK_SECRET!,
//   timestamp: req.headers["x-rmt-timestamp"] as string,
//   signatureHeader: req.headers["x-rmt-signature"] as string,
//   rawBody,
// });
// if (!ok) return res.status(401).end();

Обработчик вебхуков TypeScript

typescript
type CheckoutCompleted = {
  id: string;
  type: "checkout.completed";
  created: string;
  data: {
    checkout: {
      uid: string;
      status: "paid";
      amount: number;
      currency: string;
      itemName: string | null;
      description: string;
      invoiceId: string | null;
      source: string | null;
      orderUid: string | null;
      hostedUrl: string;
      email: string | null;
      paidAt: string | null;
      expiresAt: string | null;
      createdAt: string | null;
      reason: null;
      items: Array<{
        name: string;
        description: string | null;
        amount: number;
        quantity: number;
        imageUrl: string | null;
      }>;
    };
  };
};

type CheckoutCanceled = {
  type: "checkout.canceled";
  data: {
    checkout: {
      uid: string;
      status: "canceled" | "expired";
      invoiceId: string | null;
      reason: "buyer_canceled" | "expired";
    };
  };
};

export async function handleRmtWebhook(rawBody: string, headers: Headers) {
  const ok = verifyRmtSignature({
    secret: process.env.RMT_WEBHOOK_SECRET!,
    timestamp: headers.get("x-rmt-timestamp"),
    signatureHeader: headers.get("x-rmt-signature"),
    rawBody,
  });
  if (!ok) throw new Response("Unauthorized", { status: 401 });

  const event = JSON.parse(rawBody) as { type: string; data: Record<string, unknown> };
  switch (event.type) {
    case "checkout.completed": {
      const checkout = (event as CheckoutCompleted).data.checkout;
      if (!checkout.invoiceId || !checkout.paidAt) break;
      await fulfillShopOrder(checkout.invoiceId, checkout.orderUid);
      break;
    }
    case "checkout.canceled": {
      const checkout = (event as CheckoutCanceled).data.checkout;
      await markShopOrderCanceled(checkout.invoiceId, checkout.reason);
      break;
    }
    case "checkout.refunded":
    case "order.paid":
    case "order.delivered":
      break;
    default:
      break;
  }
}

Резервировать вебхуки (пополнение инвентаря)

Для COMPLEX (уникальных единиц) предложений RMT может отправить POST на вашу HTTPS конечную точку после оплаты, чтобы создать следующую лицензию, аккаунт или ключ, когда местного запаса недостаточно.

Ошибки, безопасные для оплаты

Если ваша конечная точка истекает или возвращает недействительные данные, заказ остается ОПЛАЧЕННЫМ. Покупатель оплачивается; вы видите ошибку в заказе и можете повторно попытаться зарезервировать или вручную прикрепить ключи.

Как это настроить

  1. Создайте СЛОЖНОЕ предложение с полями предметов (например, Лицензия).
  2. На шаге Предметы включите конечную точку инвентаря по запросу и вставьте ваш публичный HTTPS URL.
  3. При желании установите секрет подписи, чтобы RMT отправлял X-RMT-Timestamp и X-RMT-Signature при каждом вызове.
  4. Запустите тест (или вставьте пример JSON), сопоставьте пути ответа с полями предметов, затем сохраните.
  5. Опубликуйте объявление. Покупатели могут приобретать с пустым локальным запасом; ключи создаются после оплаты.
  • Локальный запас всегда предпочтителен; вебхук заполняет только нехватку.
  • Настройте уровень предложения по умолчанию или переопределите для каждого ценового варианта на этапе предметов редактора предложений.
  • Только HTTPS. Необязательная HMAC подпись соответствует исходящим вебхукам (X-RMT-Event: reserve.item).
  • Тест в редакторе отправляет dryRun: true. На странице заказа используйте Повторить резервирование после исправления вашей конечной точки.

Каноническое тело POST (усеченное)

json
{
  "id": "rsv_…",
  "type": "reserve.item",
  "order": { "uid": "ord_…", "reference": "RMT-…", "url": "https://rmt.gg/orders/ord_…" },
  "offer": { "url": "my-offer", "title": "Game key", "pageUrl": "https://rmt.gg/offers/my-offer" },
  "option": { "id": 1, "name": "Standard" },
  "fields": [{ "id": 10, "name": "License", "type": "text", "required": true }],
  "quantity": 1
}

Заголовки запроса (когда установлен секрет подписи)

json
{
  "Content-Type": "application/json",
  "X-RMT-Event": "reserve.item",
  "X-RMT-Delivery": "rsv_…",
  "X-RMT-Timestamp": "1710000000",
  "X-RMT-Signature": "v1=abc123…"
}

Удобный ответ

json
{
  "entries": [
    { "name": "License", "value": "AAAA-BBBB-CCCC" }
  ]
}

Сопоставленные поля JSON (с путями responseMap, такими как $.license)

json
{
  "license": "AAAA-BBBB-CCCC",
  "email": "[email protected]",
  "password": "temporary-pass"
}

Как проверить секрет подписи

Если вы установили секрет в предложении, каждый POST запроса резервирования подписан. Пересчитайте HMAC-SHA256(секрет, временная метка + '.' + сырое тело) и сравните с X-RMT-Signature после удаления префикса v1=. Сам секрет никогда не включается в запрос.

Смотрите полный пример проверки

Обработчик резервирования TypeScript (проверка, затем возврат записей)

typescript
type ReserveRequest = {
  id: string;
  type: "reserve.item";
  dryRun?: boolean;
  quantity: number;
  fields: Array<{ name: string; required?: boolean }>;
};

export async function handleReserve(rawBody: string, headers: Headers) {
  const ok = verifyRmtSignature({
    secret: process.env.RMT_RESERVE_SECRET!,
    timestamp: headers.get("x-rmt-timestamp"),
    signatureHeader: headers.get("x-rmt-signature"),
    rawBody,
  });
  if (!ok) return new Response("Unauthorized", { status: 401 });

  const body = JSON.parse(rawBody) as ReserveRequest;
  if (body.type !== "reserve.item") {
    return Response.json({ error: "Unexpected event" }, { status: 400 });
  }

  const qty = Number(body.quantity);
  if (!Number.isInteger(qty) || qty < 1) {
    return Response.json({ error: "Invalid quantity" }, { status: 400 });
  }

  if (body.dryRun) {
    return Response.json({
      entries: [{ name: "License", value: "TEST-AAAA-BBBB" }],
    });
  }

  const license = await mintLicense(); // your inventory
  return Response.json({
    entries: [{ name: "License", value: license }],
  });
}

Не вызывайте резервирование до оплаты

RMT вызывает вашу конечную точку только после успешной оплаты, поэтому заброшенные покупки не сжигают лицензии.

Ошибки и лимиты скорости

Ошибки возвращают JSON { error, code? }. Трафик Open API ограничен до 300 запросов в минуту на каждый API ключ.

  • API_KEY_REQUIRED
    401

    Отсутствует заголовок Authorization или X-Api-Key.

  • API_KEY_INVALID
    401

    Ключ неизвестен, отозван, истек или доступ разработчика приостановлен.

  • SCOPE_MISSING
    403

    Ключ не имеет области, требуемой конечной точкой.

  • RATE_LIMITED
    429

    Слишком много запросов. Уважайте Retry-After и X-RateLimit-Reset.

  • CHECKOUT_PARTNER_NOT_APPROVED
    403

    Этот продавец не одобрен для хостинга оформления.

  • INVALID_JSON
    400

    Тело запроса должно быть в формате JSON.

  • INVALID_AMOUNT
    400

    сумма должна быть больше 0 и не более 1,000,000.

  • UNSUPPORTED_CURRENCY
    400

    валюта не поддерживается как ISO код.

  • INVALID_RETURN_URL
    400

    returnUrl и cancelUrl должны быть https (http://localhost разрешен для локальных магазинов).

  • INVALID_PSP_CATEGORY
    400

    categorySlug и offering должны отправляться вместе и соответствовать паре в каталоге.

  • INVALID_INVOICE_ID
    400

    invoiceId длиннее 128 символов.

  • INVOICE_ID_REQUIRED
    400

    GET /checkout/sessions требует invoiceId в качестве параметра запроса.

  • INVALID_IDEMPOTENCY_KEY
    400

    Idempotency-Key длиннее 128 символов.

  • INVALID_METADATA
    400

    metadata должно быть JSON объектом, а не массивом или примитивом.

  • METADATA_TOO_LARGE
    400

    Сериализованный metadata больше 4096 символов.

  • IDEMPOTENCY_CONFLICT
    409

    Idempotency-Key был повторно использован с другой суммой, валютой или предметом.

  • INVOICE_CONFLICT
    409

    invoiceId был повторно использован с другой суммой, валютой или предметом.

  • INVALID_IMAGE_URL
    400

    imageUrl должен быть https URL.

  • ITEM_NAME_REQUIRED
    400

    Требуется itemName (или заголовок), если items не указаны.

  • INVALID_ITEMS
    400

    items должны быть непустым массивом заблокированных элементов (макс 20). Каждая строка должна содержать имя и сумму.

  • TOO_MANY_ITEMS
    400

    items не могут содержать более 20 строк.

  • AMOUNT_MISMATCH
    400

    сумма должна равняться сумме каждого элемента, умноженной на количество.

  • INVALID_DELIVERY
    400

    Поля доставки недействительны. Каждое поле должно иметь имя (макс 80) и значение (макс 2048). type должен быть text, password или textarea (по умолчанию text). Максимум 16 полей на строку.

  • ITEMS_TOO_LARGE
    400

    Сериализованный JSON предметов больше 48,000 символов.

  • NOT_FOUND
    404

    Нет хостинговой сессии чек-аут, соответствующей этому uid или invoiceId для этого продавца.

  • RESERVE_FAILED
    400

    Резервный вебхук истек, вернул недействительные данные или пропустил обязательные поля.

  • OPTION_AMBIGUOUS
    409

    Более одного варианта цен соответствует этому названию. Передайте optionId из GET stock.

  • OPTION_NOT_FOUND
    404

    Нет варианта цен, соответствующего этому id или названию в этом предложении.

  • OPTION_REQUIRED
    400

    Это предложение имеет несколько вариантов цен. Передайте option или optionId.

  • UNKNOWN_FIELD
    400

    Название поля не соответствует схеме доставки этого предложения.

  • FIELD_MAPPING_AMBIGUOUS
    400

    Не удалось сопоставить столбцы или ключи с полями доставки. Отправьте заголовки или используйте объекты элементов, ключи которых соответствуют названию поля.

  • STOCK_MODE_MISMATCH
    400

    Этот полезный груз не соответствует режиму запасов опции (количество против сохраненных предметов).

  • IMPORT_TOO_LARGE
    400

    Запрос на пополнение может импортировать максимум 1,000 сохраненных предметов на опцию.

  • OPTION_ITEM_CAPACITY
    400

    Этот вариант цен уже имеет максимум 5,000 непроданных сохраненных предметов.

  • DUPLICATE_ITEMS
    409

    onDuplicate=error и по крайней мере один предмет уже существует в этой опции.

  • UNLIMITED_STOCK
    400

    Этот вариант имеет неограниченное количество. Используйте set, чтобы сначала переключиться на конечное количество.

  • INSUFFICIENT_STOCK
    400

    Недостаточно запасов для удаления.

  • STOCK_HELD_IN_CHECKOUT
    400

    Нельзя уменьшить количество ниже единиц, которые в данный момент зарезервированы в корзине.

  • INVALID_RESTOCK
    400

    Тело пополнения не содержит обязательного действия или объединяет add/items в одной опции.

Обрабатывайте 429

Уменьшите скорость, используя Retry-After секунды. Не меняйте ключи, чтобы обойти лимиты; лимит установлен на ключ и одинаков для всех продавцов.

Успешные ответы включают X-RateLimit-Limit, X-RateLimit-Remaining и X-RateLimit-Reset.

Готовы автоматизировать?

Создайте ключ в настройках разработчика и подключите Discord или Telegram в разделе Уведомления.