/api/v1发现文档
返回作用域、配额、Webhook 事件和完整的操作目录。任何有效的 API 密钥均可使用。
卖家开放 API 适用于希望获得 Discord 警报、库存同步、Zapier 风格自动化或在 RMT.GG 上构建自定义后台的卖家。
在开发者设置中创建一个API密钥,然后调用发现以打印实时目录。
/api/v1返回作用域、配额、Webhook 事件和完整的操作目录。任何有效的 API 密钥均可使用。
在每个 /api/v1 请求中发送您的实时秘密密钥。优先使用 HTTPS。绝不要在公共客户端或浏览器包中嵌入密钥。
首选头部
Authorization: Bearer rmt_sk_live_<prefix>_<secret>备用头部
X-Api-Key: rmt_sk_live_<prefix>_<secret>可重用的 TypeScript 客户端(Bearer 认证,类型错误,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: 保留用于未来的开放API webhook管理。今天在通知中配置Discord/Telegram,并在开发者设置中配置JSON webhook。checkout:write: 创建和读取托管结账会话。需要管理员批准的合作伙伴结账。默认密钥作用域
新密钥接收 offers:read、offers:write、orders:read 和 orders:write。出站 Webhook CRUD 保留在设置 UI(会话认证)中。
产品标识符接受公共 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对产品字段的安全子集进行补丁。当配置出站 Webhook 时,发出 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与卖家 UI 相同的删除/归档规则。
请求
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)和字段名称进行补货。您不需要内部字段 ID。响应中永远不包含凭证值。
/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(最新、最旧、总高、总低)。
请求
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 webhook。付款后,我们会发出checkout.completed。交付值保留在RMT.GG确认中;它们不在卖家GET或webhook中。
/api/v1/checkout/sessions将买家引导至锁定的 RMT.GG 支付页面。一个物品:数量和物品名称。购物车:每行包含名称和数量的 items[]。货币默认为美元(USD)。支付后,如果有交付字段需要复制,买家将留在 RMT.GG;如果没有交付,我们将在短暂倒计时后将他们送回商店。数量、长度和其他限制在限制列中。
{
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仅在状态为已付款时为true。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与通过uid的GET相同的会话对象。传递您在创建时发送的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 Webhook)。当订阅事件触发时,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…"
}结账完成的有效负载
{
"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"
}
]
}
}
}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=加上十六进制)。使用你的密钥对字符串 timestamp + '.' + rawBody 计算 HMAC-SHA256,然后与 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 webhook 处理程序
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 可以在付款后向您的 HTTPS 端点发送 POST,以在本地库存不足时铸造下一个许可证、账户或密钥。
付款安全失败
如果您的端点超时或返回无效数据,订单将保持为已付款。买方将被收费;您将在订单上看到错误,并可以重试保留或手动附加密钥。
规范 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 字段(响应映射路径如 $.license)
{
"license": "AAAA-BBBB-CCCC",
"email": "[email protected]",
"password": "temporary-pass"
}如果您在产品列表上设置了密钥,则每个 reserve POST 都会被签名。重新计算 HMAC-SHA256(secret, timestamp + '.' + rawBody),并在去掉 v1= 前缀后与 X-RMT-Signature 进行比较。密钥本身从不包含在请求中。
查看完整的验证示例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? }。开放 API 流量限制为每个 API 密钥每分钟 300 次请求。
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(或标题)是必需的。
INVALID_ITEMSitems必须是一个非空的锁定行项目数组(最多20个)。每一行需要name和amount。
TOO_MANY_ITEMSitems 不能包含超过 20 行。
AMOUNT_MISMATCHamount 必须等于每行金额乘以数量的总和。
INVALID_DELIVERY交付字段无效。每个字段需要一个名称(最多80个字符)和一个值(最多2048个字符)。类型必须是text,password或textarea(默认为text)。每行最多16个字段。
ITEMS_TOO_LARGE序列化的items JSON大于48,000个字符。
NOT_FOUND没有托管结账会话与该uid或invoiceId匹配此卖家。
RESERVE_FAILED保留 Webhook 超时、返回无效数据或缺少必填字段。
OPTION_AMBIGUOUS多个定价选项与该名称匹配。请从 GET stock 中传递 optionId。
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(或标题)是必需的。 |
| INVALID_ITEMS | 400 | items必须是一个非空的锁定行项目数组(最多20个)。每一行需要name和amount。 |
| TOO_MANY_ITEMS | 400 | items 不能包含超过 20 行。 |
| AMOUNT_MISMATCH | 400 | amount 必须等于每行金额乘以数量的总和。 |
| INVALID_DELIVERY | 400 | 交付字段无效。每个字段需要一个名称(最多80个字符)和一个值(最多2048个字符)。类型必须是text,password或textarea(默认为text)。每行最多16个字段。 |
| ITEMS_TOO_LARGE | 400 | 序列化的items JSON大于48,000个字符。 |
| NOT_FOUND | 404 | 没有托管结账会话与该uid或invoiceId匹配此卖家。 |
| RESERVE_FAILED | 400 | 保留 Webhook 超时、返回无效数据或缺少必填字段。 |
| OPTION_AMBIGUOUS | 409 | 多个定价选项与该名称匹配。请从 GET stock 中传递 optionId。 |
| 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。