/api/v1Документ обнаружения
Возвращает области, квоты, события вебхуков и полный каталог операций. Любой действительный API ключ работает.
Открытый API продавца предназначен для продавцов, которые хотят получать уведомления в Discord, синхронизацию запасов, автоматизацию в стиле Zapier или кастомный бэк-офис на базе RMT.GG.
Создайте API-ключ в настройках разработчика, затем вызовите discovery, чтобы распечатать живой каталог.
/api/v1Возвращает области, квоты, события вебхуков и полный каталог операций. Любой действительный API ключ работает.
Отправляйте свой живой секретный ключ в каждом запросе /api/v1. Предпочитайте только HTTPS. Никогда не встраивайте ключи в публичные клиенты или пакеты браузера.
Предпочтительный заголовок
Authorization: Bearer rmt_sk_live_<prefix>_<secret>Альтернативный заголовок
X-Api-Key: rmt_sk_live_<prefix>_<secret>Повторно используемый клиент TypeScript (Bearer auth, типизированные ошибки, повторная попытка 429)
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: Резервировано для будущего управления вебхуками Open API. Настройте Discord/Telegram в Уведомлениях и JSON вебхуки в настройках разработчика уже сегодня.checkout:write: Создание и чтение сессий хостинг-кассы. Требуется одобрение администратора для партнерской кассы.Общие области ключей
Новые ключи получают offers:read, offers:write, orders:read и orders:write. CRUD для исходящих вебхуков остается в интерфейсе настроек (аутентификация сессии).
Идентификаторы предложений принимают публичный URL-слуг или числовой ID. Ответы не содержат внутренний ID и sellerId.
Что PATCH пока не может изменить
Строки запасов, цены опций, медиа и атрибуты управляются в редакторе продавца (или будущих конечных точках), а не через PATCH сегодня.
/api/v1/offersФильтруйте с archive=active (по умолчанию), archived или all.
Запрос
archive| Имя | В | Тип | Обязательно | Описание |
|---|---|---|---|---|
archive | query | string | Необязательно | One of "active" (default), "archived", or "all". |
/api/v1/offersСоздает пустой черновик, принадлежащий аутентифицированному продавцу. Тело не требуется.
/api/v1/offers/:urlOrIdЗагрузите по публичному URL-слугу или числовому ID. Связи (опции) могут быть включены; запасные предметы не включены.
Запрос
urlOrId| Имя | В | Тип | Обязательно | Описание |
|---|---|---|---|---|
urlOrId | path | string | обязательно | Offer.url slug or Offer.id. |
/api/v1/offers/:urlOrIdОбновите безопасный подмножество полей листинга. Вызывает offer.updated, когда настроены исходящие вебхуки.
Запрос
urlOrIdtitledescriptionvisibilitycategoryIdofferingIdthumbnailofferTypelistingMode| Имя | В | Тип | Обязательно | Описание |
|---|---|---|---|---|
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). |
/api/v1/offers/:urlOrIdТе же правила удаления/архивирования, что и в интерфейсе продавца.
Запрос
urlOrId| Имя | В | Тип | Обязательно | Описание |
|---|---|---|---|---|
urlOrId | path | string | обязательно | Offer.url slug or Offer.id. |
/api/v1/offers/:urlOrId/publishПубликует черновик (или изменяет видимость). Не удается с 400, если обязательные поля листинга неполные.
Запрос
urlOrIdvisibility| Имя | В | Тип | Обязательно | Описание |
|---|---|---|---|---|
urlOrId | path | string | обязательно | Offer.url slug or Offer.id. |
visibility | body | string | Необязательно | Optional. PUBLIC (default), PRIVATE, or UNPUBLISHED. |
Смотрите количество доступных для покупки по уровням, сопоставьте названия полей доставки с правильным предложением, затем пополните количество или сохраненные ключи и аккаунты.
Как работает сопоставление
GET /api/v1/stock?fields=username,password находит предложения, схема которых содержит эти поля. Пополните с названиями опций (или optionId) и названиями полей. Вам не нужны внутренние идентификаторы полей. Ответы никогда не содержат значения учетных данных.
/api/v1/stockВозвращает количество по уровням и названия полей доставки, чтобы вы могли сопоставить ключи и аккаунты с правильным предложением. Фильтруйте с помощью q, fields, stockMode и lowStock. Никогда не возвращает значения учетных данных.
Запрос
qfieldsstockModelowStockarchive| Имя | В | Тип | Обязательно | Лимиты | Описание |
|---|---|---|---|---|---|
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". |
/api/v1/offers/:urlOrId/stockТа же форма StockOffer, что и в индексе, для одного url или числового id. Только количество.
Запрос
urlOrId| Имя | В | Тип | Обязательно | Описание |
|---|---|---|---|---|
urlOrId | path | string | обязательно | Offer.url slug or Offer.id. |
/api/v1/offers/:urlOrId/stockУровни количества: добавить, удалить или установить. Уровни сохраненных предметов: объекты предметов по названию поля, keys[] когда есть одно поле, или текст с разделителями. Несколько уровней в одном вызове через options[]. dryRun предварительно показывает сопоставление. onDuplicate по умолчанию пропускает.
{
"option": "1 Month",
"add": 50
}Запрос
urlOrIdoptionoptionIdaddremovesetitemskeystextdelimiterheadersoptionsdryRunonDuplicate| Имя | В | Тип | Обязательно | Лимиты | Описание |
|---|---|---|---|---|---|
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. |
Используйте POST /api/v1/offers/:url/stock с items[] для аккаунтов или keys[] для лицензий с одним полем. Разбивайте на 1,000 строк за запрос.
Выберите полезную нагрузку, соответствующую объявлению
Сначала выполните GET stock. Если fields[] содержит более одного имени, отправьте объекты items с ключами по этим именам (Имя пользователя, Пароль, Электронная почта). Если есть ровно одно поле, достаточно keys[]. Для количественных объявлений используйте add, а не items.
1,000 строк на запрос. 5,000 непроданных предметов на уровень. 300 запросов в минуту. Дубликаты пропускаются по умолчанию.
accounts.json (по одному объекту на аккаунт)
[
{ "Username": "player1", "Password": "secret1", "E-Mail": "[email protected]" },
{ "Username": "player2", "Password": "secret2", "E-Mail": "[email protected]" }
]accounts.csv
Username,Password,E-Mail
player1,secret1,p1@example.com
player2,secret2,p2@example.comkeys.txt (по одному лицензионному ключу на строку)
AAAA-BBBB-CCCC
DDDD-EEEE-FFFF
GGGG-HHHH-IIIIcURL
# 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 строк за запрос)
// 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/v1/ordersПоддерживает limit, offset, status, q и sort (новейшие, старейшие, total_high, total_low).
Запрос
limitoffsetstatusqsort| Имя | В | Тип | Обязательно | Лимиты | Описание |
|---|---|---|---|---|---|
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. |
/api/v1/orders/:uidВозвращает заказ с позициями. Используйте публичный uid заказа.
Запрос
uid| Имя | В | Тип | Обязательно | Описание |
|---|---|---|---|---|
uid | path | string | обязательно | Order.uid. |
/api/v1/orders/:uid/deliverРучное выполнение. COMPLEX строки должны быть полностью прикреплены, когда это требуется. Вызывает order.delivered.
Запрос
uidevidence| Имя | В | Тип | Обязательно | Лимиты | Описание |
|---|---|---|---|---|---|
uid | path | string | обязательно | Order.uid. | |
evidence | body | string[] | Необязательно | HTTPS, max 10 | Optional screenshot or transfer-proof URLs. |
Любой одобренный партнерский магазин или бэкенд могут отправить покупателей на страницу оплаты RMT.GG. Мы остаемся продавцом и берем 4% от заблокированной суммы.
Разрешенные и выполнение
Перейдите в Настройки, Хостинговый чек-аут, затем создайте API-ключ и JSON вебхук. После оплаты мы отправляем checkout.completed. Значения доставки остаются в подтверждении RMT.GG; их нет в GET продавца или вебхуках.
/api/v1/checkout/sessionsПеренаправьте покупателей на заблокированную страницу оплаты RMT.GG. Один предмет: сумма и itemName. Корзина: items[] с названием и суммой в каждой строке. Валюта по умолчанию - USD. После оплаты покупатель остается на RMT.GG, когда есть поля для доставки, которые нужно скопировать. returnUrl ведет обратно в магазин; при отсутствии доставки мы возвращаем их после короткого обратного отсчета. Суммы, длины и другие ограничения указаны в колонке Limits.
{
amount: 10, // what the buyer pays
itemName: "Gold pack", // pay page heading
}Запрос
amountcurrencyitemNametitledescriptionimageUrlitemsitems[].nameitems[].titleitems[].descriptionitems[].amountitems[].quantityitems[].imageUrlitems[].deliveryitems[].delivery[].nameitems[].delivery[].typeitems[].delivery[].valueemailreturnUrlcancelUrlinvoiceIdcategorySlugofferingmetadataIdempotency-Key| Имя | В | Тип | Обязательно | Лимиты | Описание |
|---|---|---|---|---|---|
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. |
Ответ
uidstatuspaidamountcurrencyitemNamedescriptionemaillangreturnUrlcancelUrlinvoiceIdexternalInvoiceIdsourceexpiresAthostedUrlorderUiditemshosted_urlexpires_at| Имя | В | Тип | Описание |
|---|---|---|---|
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. |
/api/v1/checkout/sessions/:uidВозвращает сессию, которую вы создали. Используйте это, если checkout.completed задерживается. paid истинно только когда статус оплачен. items никогда не включают значения доставки.
Запрос
uid| Имя | В | Тип | Обязательно | Описание |
|---|---|---|---|---|
uid | path | string | обязательно | Session uid returned at create time. |
Ответ
uidstatuspaidamountcurrencyitemNamedescriptionemaillangreturnUrlcancelUrlinvoiceIdexternalInvoiceIdsourceexpiresAthostedUrlorderUiditems| Имя | В | Тип | Описание |
|---|---|---|---|
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. |
/api/v1/checkout/sessionsТот же объект сессии, что и GET по uid. Передайте invoiceId, который вы отправили при создании. Отсутствует: 400 INVOICE_ID_REQUIRED. Неизвестно: 404 NOT_FOUND.
Запрос
invoiceId| Имя | В | Тип | Обязательно | Лимиты | Описание |
|---|---|---|---|---|---|
invoiceId | query | string | обязательно | Max 128 | invoiceId from create. Missing: 400 INVOICE_ID_REQUIRED. Too long: 400 INVALID_INVOICE_ID. |
Ответ
uidstatuspaidamountcurrencyitemNamedescriptionemaillangreturnUrlcancelUrlinvoiceIdexternalInvoiceIdsourceexpiresAthostedUrlorderUiditems| Имя | В | Тип | Описание |
|---|---|---|---|
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. |
Настройте HTTPS конечные точки (или вебхуки Discord) в Настройки → Разработчик. RMT отправляет POST, когда срабатывают подписанные события.
Конверт доставки 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 */ ]
}
}
}Подписанные заголовки доставки
{
"X-RMT-Event": "order.paid",
"X-RMT-Delivery": "whd_…",
"X-RMT-Timestamp": "1710000000",
"X-RMT-Signature": "v1=abc123…"
}payload завершения покупки
{
"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
{
"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 минут.
Проверка TypeScript (безопасное сравнение по времени и окно повторной передачи 5 минут)
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
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 конечную точку после оплаты, чтобы создать следующую лицензию, аккаунт или ключ, когда местного запаса недостаточно.
Ошибки, безопасные для оплаты
Если ваша конечная точка истекает или возвращает недействительные данные, заказ остается ОПЛАЧЕННЫМ. Покупатель оплачивается; вы видите ошибку в заказе и можете повторно попытаться зарезервировать или вручную прикрепить ключи.
Каноническое тело POST (усеченное)
{
"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
}Заголовки запроса (когда установлен секрет подписи)
{
"Content-Type": "application/json",
"X-RMT-Event": "reserve.item",
"X-RMT-Delivery": "rsv_…",
"X-RMT-Timestamp": "1710000000",
"X-RMT-Signature": "v1=abc123…"
}Удобный ответ
{
"entries": [
{ "name": "License", "value": "AAAA-BBBB-CCCC" }
]
}Сопоставленные поля JSON (с путями responseMap, такими как $.license)
{
"license": "AAAA-BBBB-CCCC",
"email": "[email protected]",
"password": "temporary-pass"
}Если вы установили секрет в предложении, каждый POST запроса резервирования подписан. Пересчитайте HMAC-SHA256(секрет, временная метка + '.' + сырое тело) и сравните с X-RMT-Signature после удаления префикса v1=. Сам секрет никогда не включается в запрос.
Смотрите полный пример проверкиОбработчик резервирования 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Отсутствует заголовок Authorization или X-Api-Key.
API_KEY_INVALIDКлюч неизвестен, отозван, истек или доступ разработчика приостановлен.
SCOPE_MISSINGКлюч не имеет области, требуемой конечной точкой.
RATE_LIMITEDСлишком много запросов. Уважайте Retry-After и X-RateLimit-Reset.
CHECKOUT_PARTNER_NOT_APPROVEDЭтот продавец не одобрен для хостинга оформления.
INVALID_JSONТело запроса должно быть в формате JSON.
INVALID_AMOUNTсумма должна быть больше 0 и не более 1,000,000.
UNSUPPORTED_CURRENCYвалюта не поддерживается как ISO код.
INVALID_RETURN_URLreturnUrl и cancelUrl должны быть https (http://localhost разрешен для локальных магазинов).
INVALID_PSP_CATEGORYcategorySlug и offering должны отправляться вместе и соответствовать паре в каталоге.
INVALID_INVOICE_IDinvoiceId длиннее 128 символов.
INVOICE_ID_REQUIREDGET /checkout/sessions требует invoiceId в качестве параметра запроса.
INVALID_IDEMPOTENCY_KEYIdempotency-Key длиннее 128 символов.
INVALID_METADATAmetadata должно быть JSON объектом, а не массивом или примитивом.
METADATA_TOO_LARGEСериализованный metadata больше 4096 символов.
IDEMPOTENCY_CONFLICTIdempotency-Key был повторно использован с другой суммой, валютой или предметом.
INVOICE_CONFLICTinvoiceId был повторно использован с другой суммой, валютой или предметом.
INVALID_IMAGE_URLimageUrl должен быть https URL.
ITEM_NAME_REQUIREDТребуется itemName (или заголовок), если items не указаны.
INVALID_ITEMSitems должны быть непустым массивом заблокированных элементов (макс 20). Каждая строка должна содержать имя и сумму.
TOO_MANY_ITEMSitems не могут содержать более 20 строк.
AMOUNT_MISMATCHсумма должна равняться сумме каждого элемента, умноженной на количество.
INVALID_DELIVERYПоля доставки недействительны. Каждое поле должно иметь имя (макс 80) и значение (макс 2048). type должен быть text, password или textarea (по умолчанию text). Максимум 16 полей на строку.
ITEMS_TOO_LARGEСериализованный JSON предметов больше 48,000 символов.
NOT_FOUNDНет хостинговой сессии чек-аут, соответствующей этому uid или invoiceId для этого продавца.
RESERVE_FAILEDРезервный вебхук истек, вернул недействительные данные или пропустил обязательные поля.
OPTION_AMBIGUOUSБолее одного варианта цен соответствует этому названию. Передайте optionId из GET stock.
OPTION_NOT_FOUNDНет варианта цен, соответствующего этому id или названию в этом предложении.
OPTION_REQUIREDЭто предложение имеет несколько вариантов цен. Передайте option или optionId.
UNKNOWN_FIELDНазвание поля не соответствует схеме доставки этого предложения.
FIELD_MAPPING_AMBIGUOUSНе удалось сопоставить столбцы или ключи с полями доставки. Отправьте заголовки или используйте объекты элементов, ключи которых соответствуют названию поля.
STOCK_MODE_MISMATCHЭтот полезный груз не соответствует режиму запасов опции (количество против сохраненных предметов).
IMPORT_TOO_LARGEЗапрос на пополнение может импортировать максимум 1,000 сохраненных предметов на опцию.
OPTION_ITEM_CAPACITYЭтот вариант цен уже имеет максимум 5,000 непроданных сохраненных предметов.
DUPLICATE_ITEMSonDuplicate=error и по крайней мере один предмет уже существует в этой опции.
UNLIMITED_STOCKЭтот вариант имеет неограниченное количество. Используйте set, чтобы сначала переключиться на конечное количество.
INSUFFICIENT_STOCKНедостаточно запасов для удаления.
STOCK_HELD_IN_CHECKOUTНельзя уменьшить количество ниже единиц, которые в данный момент зарезервированы в корзине.
INVALID_RESTOCKТело пополнения не содержит обязательного действия или объединяет add/items в одной опции.
| Код | HTTP | Описание |
|---|---|---|
| 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 в разделе Уведомления.