/api/v1Documento de descoberta
Retorna escopos, cotas, eventos de webhook e o catálogo completo de operações. Qualquer chave de API válida funciona.
A API Aberta do Vendedor é para vendedores que desejam alertas do Discord, sincronização de estoque, automação estilo Zapier ou um back office personalizado em cima do RMT.GG.
Crie uma chave de API nas configurações de Desenvolvedor e, em seguida, chame a descoberta para imprimir o catálogo ao vivo.
/api/v1Retorna escopos, cotas, eventos de webhook e o catálogo completo de operações. Qualquer chave de API válida funciona.
Envie sua chave secreta ao vivo em cada requisição /api/v1. Prefira apenas HTTPS. Nunca insira chaves em clientes públicos ou pacotes de navegador.
Cabeçalho preferido
Authorization: Bearer rmt_sk_live_<prefix>_<secret>Cabeçalho alternativo
X-Api-Key: rmt_sk_live_<prefix>_<secret>Cliente TypeScript reutilizável (autenticação Bearer, erros tipados, 429 retry)
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));
}
}
}
Rotacione em caso de vazamento
Se uma chave vazar, revogue-a nas configurações de Desenvolvedor e crie uma nova. Atualize sua automação antes de revogar se você estiver ao vivo.
Cada chave de API possui escopos que controlam os endpoints. Escopo ausente retorna 403 SCOPE_MISSING.
offers:read: Listar e obter suas ofertas.offers:write: Criar, atualizar, publicar e excluir ofertas.orders:read: Listar e obter pedidos de vendedores.orders:write: Marcar pedidos como entregues.webhooks:manage: Reservado para gerenciamento futuro de webhooks da Open API. Configure o Discord/Telegram em Notificações e webhooks JSON nas configurações de Desenvolvedor hoje.checkout:write: Criar e ler sessões de checkout hospedadas. Requer checkout de parceiro aprovado pelo admin.Escopos padrão da chave
Novas chaves recebem offers:read, offers:write, orders:read e orders:write. CRUD de webhook de saída permanece na UI de Configurações (autenticação de sessão).
Identificadores de oferta aceitam o slug da URL pública ou id numérico. Respostas omitem id interno e sellerId.
O que o PATCH ainda não pode mudar
Linhas de estoque, preços de opções, mídia e atributos são gerenciados no editor de vendedores (ou futuros endpoints), não via PATCH hoje.
/api/v1/offersFiltre com archive=active (padrão), archived ou all.
Requisição
archive| Nome | Em | Tipo | Necessário | Descrição |
|---|---|---|---|---|
archive | query | string | Opcional | One of "active" (default), "archived", or "all". |
/api/v1/offersCria um rascunho vazio pertencente ao vendedor autenticado. Nenhum corpo é necessário.
/api/v1/offers/:urlOrIdCarrega pelo slug da URL pública ou id numérico. Relações (opções) podem ser incluídas; itens de estoque não são.
Requisição
urlOrId| Nome | Em | Tipo | Necessário | Descrição |
|---|---|---|---|---|
urlOrId | path | string | obrigatório | Offer.url slug or Offer.id. |
/api/v1/offers/:urlOrIdPatch um subconjunto seguro de campos de listagem. Emite offer.updated quando webhooks de saída estão configurados.
Requisição
urlOrIdtitledescriptionvisibilitycategoryIdofferingIdthumbnailofferTypelistingMode| Nome | Em | Tipo | Necessário | Descrição |
|---|---|---|---|---|
urlOrId | path | string | obrigatório | Offer.url slug or Offer.id. |
title | body | string | Opcional | Listing title. |
description | body | string | Opcional | Listing description. |
visibility | body | string | Opcional | PUBLIC | PRIVATE | UNPUBLISHED. |
categoryId | body | number | Opcional | Catalog category id. |
offeringId | body | number | Opcional | Catalog offering id. |
thumbnail | body | string | Opcional | Thumbnail URL or asset reference. |
offerType | body | string | Opcional | Offer type string used by the listing. |
listingMode | body | string | Opcional | Listing mode (for example STANDARD, RANK_BOOST, SESSION). |
/api/v1/offers/:urlOrIdMesmas regras de exclusão/arquivamento que a UI do vendedor.
Requisição
urlOrId| Nome | Em | Tipo | Necessário | Descrição |
|---|---|---|---|---|
urlOrId | path | string | obrigatório | Offer.url slug or Offer.id. |
/api/v1/offers/:urlOrId/publishPublica um rascunho (ou altera a visibilidade). Falha com 400 se campos obrigatórios da listagem estiverem incompletos.
Requisição
urlOrIdvisibility| Nome | Em | Tipo | Necessário | Descrição |
|---|---|---|---|---|
urlOrId | path | string | obrigatório | Offer.url slug or Offer.id. |
visibility | body | string | Opcional | Optional. PUBLIC (default), PRIVATE, or UNPUBLISHED. |
Veja as quantidades disponíveis para compra por nível, combine os nomes dos campos de entrega com a listagem correta e, em seguida, reabasteça a quantidade ou as chaves e contas salvas.
Como funciona a correspondência
GET /api/v1/stock?fields=username,password encontra listagens cujo esquema possui esses campos. Reabasteça com nomes de opções (ou optionId) e nomes de campos. Você não precisa de ids de campo internos. As respostas nunca incluem valores de credenciais.
/api/v1/stockRetorna contagens por nível e nomes dos campos de entrega para que você possa combinar chaves e contas com a oferta correta. Filtre com q, fields, stockMode e lowStock. Nunca retorna valores de credenciais.
Requisição
qfieldsstockModelowStockarchive| Nome | Em | Tipo | Necessário | Limites | Descrição |
|---|---|---|---|---|---|
q | query | string | Opcional | Max 80 | Filter by listing title or url slug. |
fields | query | string | Opcional | Comma-separated delivery field names. The listing must have all of them (Username,Password). Names match case-insensitively. | |
stockMode | query | string | Opcional | QUANTITY or COMPLEX. Listing must have at least one option in that mode. | |
lowStock | query | number | Opcional | Keep listings that have a finite tier with available less than or equal to this number. | |
archive | query | string | Opcional | One of "active" (default), "archived", or "all". |
/api/v1/offers/:urlOrId/stockMesma estrutura de StockOffer que o índice, para uma url ou id numérico. Apenas contagens.
Requisição
urlOrId| Nome | Em | Tipo | Necessário | Descrição |
|---|---|---|---|---|
urlOrId | path | string | obrigatório | Offer.url slug or Offer.id. |
/api/v1/offers/:urlOrId/stockNíveis de quantidade: adicionar, remover ou definir. Níveis de itens salvos: objetos de itens por nome de campo, keys[] quando há um campo, ou texto delimitado. Vários níveis em uma chamada via options[]. dryRun pré-visualiza a correspondência. onDuplicate padrão é pular.
{
"option": "1 Month",
"add": 50
}Requisição
urlOrIdoptionoptionIdaddremovesetitemskeystextdelimiterheadersoptionsdryRunonDuplicate| Nome | Em | Tipo | Necessário | Limites | Descrição |
|---|---|---|---|---|---|
urlOrId | path | string | obrigatório | Offer.url slug or Offer.id. | |
option | body | string | Condicional | Pricing option name (case-insensitive). Omit when the listing has a single tier. | |
optionId | body | number | Condicional | Pricing option id from GET stock. Wins over option when both are sent. Ambiguous names return 409 OPTION_AMBIGUOUS. | |
add | body | number | Condicional | 1-1,000,000 | QUANTITY: add this many units. Fails with 400 UNLIMITED_STOCK if the tier is unlimited. |
remove | body | number | Condicional | 1-1,000,000 | QUANTITY: withdraw this many units. Fails with 400 INSUFFICIENT_STOCK when there is not enough. |
set | body | number | null | Condicional | QUANTITY: set an absolute count. null means unlimited. Cannot go below units held in checkout. | |
items | body | object[] | Condicional | Max 1,000 | COMPLEX: objects keyed by delivery field name, for example { "Username": "a", "Password": "b" }. Names match case-insensitively. |
keys | body | string[] | Condicional | Max 1,000 | COMPLEX: license keys when the listing has exactly one delivery field. Otherwise 400 FIELD_MAPPING_AMBIGUOUS. |
text | body | string | Condicional | 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 | Opcional | Default : | Delimiter for text. Ignored unless text is sent. |
headers | body | string[] | Opcional | Optional column headers for text when the first line is data, not names. | |
options | body | object[] | Condicional | Restock several tiers in one call. Each element is the same shape as a single-option body (option, add, items, …). | |
dryRun | body | boolean | Opcional | Preview matching and counts without writing. Default false. | |
onDuplicate | body | string | Opcional | skip (default) or error | COMPLEX: skip existing unsold fingerprints, or fail the request with 409 DUPLICATE_ITEMS. |
Use POST /api/v1/offers/:url/stock com items[] para contas ou keys[] para códigos de licença de campo único. Divida em 1.000 linhas por solicitação.
Escolha o payload que corresponde à oferta
Chame GET stock primeiro. Se fields[] tiver mais de um nome, envie objetos items indexados por esses nomes (Nome de Usuário, Senha, E-Mail). Se houver exatamente um campo, keys[] é suficiente. Listagens de quantidade usam add, não items.
1.000 linhas por solicitação. 5.000 itens não vendidos por nível. 300 solicitações por minuto. Duplicatas são puladas por padrão.
accounts.json (um objeto por conta)
[
{ "Username": "player1", "Password": "secret1", "E-Mail": "[email protected]" },
{ "Username": "player2", "Password": "secret2", "E-Mail": "[email protected]" }
]contas.csv
Username,Password,E-Mail
player1,secret1,p1@example.com
player2,secret2,p2@example.comkeys.txt (uma chave de licença por linha)
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]"}'Importação em lote TypeScript (1.000 linhas por solicitação)
// 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) }),
}),
);
}
}
O que esta API protege
Chaves precisam de offers:write, têm limite de taxa e só podem reabastecer suas próprias listagens. GET nunca retorna credenciais salvas. Respostas POST não ecoam Nome de Usuário, Senha ou valores de chave. Envie o corpo via HTTPS em produção e mantenha a chave da API em uma variável de ambiente.
No Windows, use curl.exe (não o alias curl). Coloque o JSON entre aspas para que o PowerShell não o divida.
Os pedidos estão vinculados à sua conta de vendedor. Os detalhes de cobrança do comprador podem ser ocultados sob as regras de privacidade do marketplace.
/api/v1/ordersSuporta limite, offset, status, q e sort (mais novo, mais antigo, total_alto, total_baixo).
Requisição
limitoffsetstatusqsort| Nome | Em | Tipo | Necessário | Limites | Descrição |
|---|---|---|---|---|---|
limit | query | number | Opcional | 1-100, default 20 | Page size. |
offset | query | number | Opcional | >= 0, default 0 | Skip this many rows. |
status | query | string | Opcional | Max 32 | Filter by order status (for example PAID, DELIVERED, COMPLETED). |
q | query | string | Opcional | Max 80 | Search reference or related text. |
sort | query | string | Opcional | newest (default) | newest | oldest | total_high | total_low. |
/api/v1/orders/:uidRetorna o pedido com itens de linha. Use o uid público do pedido.
Requisição
uid| Nome | Em | Tipo | Necessário | Descrição |
|---|---|---|---|---|
uid | path | string | obrigatório | Order.uid. |
/api/v1/orders/:uid/deliverCumprimento manual. Linhas COMPLEX devem estar totalmente anexadas quando necessário. Emite order.delivered.
Requisição
uidevidence| Nome | Em | Tipo | Necessário | Limites | Descrição |
|---|---|---|---|---|---|
uid | path | string | obrigatório | Order.uid. | |
evidence | body | string[] | Opcional | HTTPS, max 10 | Optional screenshot or transfer-proof URLs. |
Qualquer loja parceira aprovada ou backend pode enviar compradores para uma página de pagamento do RMT.GG. Nós permanecemos como comerciante registrado e cobramos 4% do valor bloqueado.
Lista de permissão e cumprimento
Aplique em Configurações, Checkout hospedado, e então crie uma chave de API e um webhook JSON lá. Após o pagamento, emitimos checkout.completed. Os valores de entrega ficam na confirmação da RMT.GG; eles não estão no GET do vendedor ou nos webhooks.
/api/v1/checkout/sessionsEnvie os compradores para uma página de pagamento bloqueada do RMT.GG. Um item: quantidade e itemName. Carrinho: items[] com nome e quantidade em cada linha. A moeda padrão é USD. Após o pagamento, o comprador permanece no RMT.GG quando há campos de entrega para copiar. returnUrl continua para a loja; sem entrega, nós os enviamos de volta após uma contagem regressiva curta. Quantidade, comprimentos e outros limites estão na coluna Limites.
{
amount: 10, // what the buyer pays
itemName: "Gold pack", // pay page heading
}Requisição
amountcurrencyitemNametitledescriptionimageUrlitemsitems[].nameitems[].titleitems[].descriptionitems[].amountitems[].quantityitems[].imageUrlitems[].deliveryitems[].delivery[].nameitems[].delivery[].typeitems[].delivery[].valueemailreturnUrlcancelUrlinvoiceIdcategorySlugofferingmetadataIdempotency-Key| Nome | Em | Tipo | Necessário | Limites | Descrição |
|---|---|---|---|---|---|
amount | body | number | Condicional | > 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 | Opcional | Default USD | ISO 4217 code such as USD or EUR. |
itemName | body | string | Condicional | 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 | Opcional | Alias of itemName. If both are sent, itemName wins. | |
description | body | string | Opcional | Max 200 | Copy under the heading. If omitted, the heading is reused. |
imageUrl | body | string | Opcional | HTTPS, max 2048 | Product image, or fallback for lines without imageUrl. Invalid: 400 INVALID_IMAGE_URL. |
items | body | object[] | Condicional | 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 | obrigatório | Max 120 | Line title. Alias: title. |
items[].title | body | string | Opcional | Alias of items[].name. If both are sent, name wins. | |
items[].description | body | string | Opcional | Max 200 | Line copy under the name. |
items[].amount | body | number | obrigatório | > 0, max 1,000,000 | Unit price. Session total is sum(amount * quantity). |
items[].quantity | body | number | Opcional | 1-99, default 1 | Locked on the pay page. |
items[].imageUrl | body | string | Opcional | HTTPS, max 2048 | Line image. Falls back to top-level imageUrl. |
items[].delivery | body | object[] | Opcional | Max 16 fields | Shown after payment on RMT.GG. Seller GET and webhooks omit values. |
items[].delivery[].name | body | string | obrigatório | Max 80 | Field label, for example Code or Password. |
items[].delivery[].type | body | string | Opcional | text, password, textarea | password is blurred until the buyer reveals it. Default text. |
items[].delivery[].value | body | string | obrigatório | Max 2048 | Field value. Numbers are stored as strings. Empty: 400 INVALID_DELIVERY. |
email | body | string | Opcional | Invalid values ignored | Prefills the pay page. The buyer still confirms email before paying. |
returnUrl | body | string | Opcional | 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 | Opcional | HTTPS, max 2048 | Redirect if the buyer cancels or the session expires. If omitted, they stay on the pay page. |
invoiceId | body | string | Opcional | Max 128 | Your shop id. Same payload returns the existing session. A different payload: 409 INVOICE_CONFLICT. |
categorySlug | body | string | Condicional | 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 | Condicional | With categorySlug, or omit both | Catalog offering such as Mods. Mapped to labels like Games · Add-ons. |
metadata | body | object | Opcional | Object, max 4096 chars | Stored on the session. Not returned on seller GET. |
Idempotency-Key | header | string | Opcional | Max 128 | Replay header. Same key and payload returns the existing session. A different payload: 409 IDEMPOTENCY_CONFLICT. |
Resposta
uidstatuspaidamountcurrencyitemNamedescriptionemaillangreturnUrlcancelUrlinvoiceIdexternalInvoiceIdsourceexpiresAthostedUrlorderUiditemshosted_urlexpires_at| Nome | Em | Tipo | Descrição |
|---|---|---|---|
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/:uidRetorna a sessão que você criou. Use isso se checkout.completed estiver atrasado. paid é verdadeiro apenas quando o status está pago. itens nunca incluem valores de entrega.
Requisição
uid| Nome | Em | Tipo | Necessário | Descrição |
|---|---|---|---|---|
uid | path | string | obrigatório | Session uid returned at create time. |
Resposta
uidstatuspaidamountcurrencyitemNamedescriptionemaillangreturnUrlcancelUrlinvoiceIdexternalInvoiceIdsourceexpiresAthostedUrlorderUiditems| Nome | Em | Tipo | Descrição |
|---|---|---|---|
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/sessionsMesmo objeto de sessão que GET por uid. Passe o invoiceId que você enviou na criação. Faltando: 400 INVOICE_ID_REQUIRED. Desconhecido: 404 NOT_FOUND.
Requisição
invoiceId| Nome | Em | Tipo | Necessário | Limites | Descrição |
|---|---|---|---|---|---|
invoiceId | query | string | obrigatório | Max 128 | invoiceId from create. Missing: 400 INVOICE_ID_REQUIRED. Too long: 400 INVALID_INVOICE_ID. |
Resposta
uidstatuspaidamountcurrencyitemNamedescriptionemaillangreturnUrlcancelUrlinvoiceIdexternalInvoiceIdsourceexpiresAthostedUrlorderUiditems| Nome | Em | Tipo | Descrição |
|---|---|---|---|
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. |
Configure endpoints HTTPS (ou webhooks do Discord) em Configurações → Desenvolvedor. O RMT POSTa quando eventos inscritos ocorrem.
Envelope de entrega 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 */ ]
}
}
}Cabeçalhos de entrega assinados
{
"X-RMT-Event": "order.paid",
"X-RMT-Delivery": "whd_…",
"X-RMT-Timestamp": "1710000000",
"X-RMT-Signature": "v1=abc123…"
}payload de checkout.concluído
{
"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"
}
]
}
}
}Carga útil de 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"
}
]
}
}
}Quando uma chave secreta de assinatura está definida, calcule HMAC-SHA256 sobre timestamp + '.' + rawBody e compare com o hex após v1=.
O segredo permanece na RMT. Cada POST assinado inclui X-RMT-Timestamp (segundos Unix) e X-RMT-Signature (v1= mais hex). Calcule HMAC-SHA256 sobre a string timestamp + '.' + rawBody usando seu segredo, depois compare com o hex após v1=. Rejeite timestamps mais antigos que 5 minutos.
Verificação TypeScript (comparação segura em tempo e janela de repetição de 5 minutos)
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();
Manipulador de webhook 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;
}
}
Para listagens COMPLEX (unidade única), o RMT pode POSTar seu endpoint HTTPS após o pagamento para criar a próxima licença, conta ou chave quando o estoque local estiver baixo.
Falhas seguras de pagamento
Se seu endpoint expirar ou retornar dados inválidos, o pedido permanece PAGO. O comprador é cobrado; você verá um erro no pedido e poderá tentar reservar novamente ou anexar chaves manualmente.
Corpo POST canônico (truncado)
{
"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
}Cabeçalhos da solicitação (quando um segredo de assinatura está definido)
{
"Content-Type": "application/json",
"X-RMT-Event": "reserve.item",
"X-RMT-Delivery": "rsv_…",
"X-RMT-Timestamp": "1710000000",
"X-RMT-Signature": "v1=abc123…"
}Resposta de conveniência
{
"entries": [
{ "name": "License", "value": "AAAA-BBBB-CCCC" }
]
}Campos JSON mapeados (com caminhos responseMap como $.license)
{
"license": "AAAA-BBBB-CCCC",
"email": "[email protected]",
"password": "temporary-pass"
}Se você definiu um segredo na oferta, cada POST de reserva é assinado. Recalcule HMAC-SHA256(segredo, timestamp + '.' + corpoBruto) e compare com X-RMT-Signature após remover o prefixo v1=. O segredo em si nunca é incluído na solicitação.
Veja o exemplo completo de verificaçãoManipulador de reserva TypeScript (verificar, depois retornar entradas)
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 }],
});
}
Não chame reserva antes do pagamento
O RMT só chama seu endpoint após o pagamento ser bem-sucedido, então checkouts abandonados não consomem licenças.
Erros retornam JSON { error, code? }. O tráfego da API Aberta é limitado a 300 requisições por minuto por chave de API.
API_KEY_REQUIREDCabeçalho Authorization ou X-Api-Key ausente.
API_KEY_INVALIDChave desconhecida, revogada, expirada ou acesso de desenvolvedor suspenso.
SCOPE_MISSINGA chave não possui o escopo exigido pelo endpoint.
RATE_LIMITEDMuitas requisições. Respeite Retry-After e X-RateLimit-Reset.
CHECKOUT_PARTNER_NOT_APPROVEDEste vendedor não está aprovado para checkout hospedado.
INVALID_JSONO corpo da solicitação deve ser JSON.
INVALID_AMOUNTo valor deve ser maior que 0 e no máximo 1.000.000.
UNSUPPORTED_CURRENCYa moeda não é um código ISO suportado.
INVALID_RETURN_URLreturnUrl e cancelUrl devem ser https (http://localhost é permitido para lojas locais).
INVALID_PSP_CATEGORYcategorySlug e offering devem ser enviados juntos e corresponder a um par de catálogo.
INVALID_INVOICE_IDinvoiceId é maior que 128 caracteres.
INVOICE_ID_REQUIREDGET /checkout/sessions requer invoiceId como um parâmetro de consulta.
INVALID_IDEMPOTENCY_KEYIdempotency-Key é maior que 128 caracteres.
INVALID_METADATAmetadata deve ser um objeto JSON, não um array ou primitivo.
METADATA_TOO_LARGEA metadata serializada é maior que 4096 caracteres.
IDEMPOTENCY_CONFLICTIdempotency-Key foi reutilizado com um valor, moeda ou item diferente.
INVOICE_CONFLICTinvoiceId foi reutilizado com um valor, moeda ou item diferente.
INVALID_IMAGE_URLimageUrl deve ser uma URL https.
ITEM_NAME_REQUIREDitemName (ou título) é obrigatório quando items é omitido.
INVALID_ITEMSitems deve ser um array não vazio de itens bloqueados (máx. 20). Cada linha precisa de nome e amount.
TOO_MANY_ITEMSitens não podem conter mais de 20 linhas.
AMOUNT_MISMATCHa quantidade deve ser igual à soma de cada valor da linha multiplicado pela quantidade.
INVALID_DELIVERYOs campos de entrega são inválidos. Cada campo precisa de um nome (máx. 80) e valor (máx. 2048). type deve ser text, password ou textarea (padrão text). Máx. 16 campos por linha.
ITEMS_TOO_LARGEO JSON dos itens serializados é maior que 48.000 caracteres.
NOT_FOUNDNenhuma sessão de checkout hospedado corresponde a esse uid ou invoiceId para este vendedor.
RESERVE_FAILEDWebhook de reserva expirou, retornou dados inválidos ou faltaram campos obrigatórios.
OPTION_AMBIGUOUSMais de uma opção de preço corresponde a esse nome. Passe optionId do GET stock.
OPTION_NOT_FOUNDNenhuma opção de preço corresponde a esse id ou nome nesta listagem.
OPTION_REQUIREDEsta listagem tem várias opções de preço. Passe option ou optionId.
UNKNOWN_FIELDUm nome de campo não corresponde ao esquema de entrega desta listagem.
FIELD_MAPPING_AMBIGUOUSNão foi possível mapear colunas ou chaves para os campos de entrega. Envie cabeçalhos ou use objetos de itens indexados pelo nome do campo.
STOCK_MODE_MISMATCHEsse payload não corresponde ao modo de estoque da opção (quantidade vs itens salvos).
IMPORT_TOO_LARGEUma solicitação de reabastecimento pode importar no máximo 1.000 itens salvos por opção.
OPTION_ITEM_CAPACITYEsta opção de preço já possui o máximo de 5.000 itens salvos não vendidos.
DUPLICATE_ITEMSonDuplicate=error e pelo menos um item já existe nesta opção.
UNLIMITED_STOCKEsta opção tem quantidade ilimitada. Use set para mudar para uma contagem finita primeiro.
INSUFFICIENT_STOCKQuantidade de estoque insuficiente para remover.
STOCK_HELD_IN_CHECKOUTNão é possível reduzir a quantidade abaixo das unidades atualmente reservadas no checkout.
INVALID_RESTOCKO corpo do reabastecimento está faltando uma ação obrigatória ou combina add/items em uma opção.
| Código | HTTP | Descrição |
|---|---|---|
| API_KEY_REQUIRED | 401 | Cabeçalho Authorization ou X-Api-Key ausente. |
| API_KEY_INVALID | 401 | Chave desconhecida, revogada, expirada ou acesso de desenvolvedor suspenso. |
| SCOPE_MISSING | 403 | A chave não possui o escopo exigido pelo endpoint. |
| RATE_LIMITED | 429 | Muitas requisições. Respeite Retry-After e X-RateLimit-Reset. |
| CHECKOUT_PARTNER_NOT_APPROVED | 403 | Este vendedor não está aprovado para checkout hospedado. |
| INVALID_JSON | 400 | O corpo da solicitação deve ser JSON. |
| INVALID_AMOUNT | 400 | o valor deve ser maior que 0 e no máximo 1.000.000. |
| UNSUPPORTED_CURRENCY | 400 | a moeda não é um código ISO suportado. |
| INVALID_RETURN_URL | 400 | returnUrl e cancelUrl devem ser https (http://localhost é permitido para lojas locais). |
| INVALID_PSP_CATEGORY | 400 | categorySlug e offering devem ser enviados juntos e corresponder a um par de catálogo. |
| INVALID_INVOICE_ID | 400 | invoiceId é maior que 128 caracteres. |
| INVOICE_ID_REQUIRED | 400 | GET /checkout/sessions requer invoiceId como um parâmetro de consulta. |
| INVALID_IDEMPOTENCY_KEY | 400 | Idempotency-Key é maior que 128 caracteres. |
| INVALID_METADATA | 400 | metadata deve ser um objeto JSON, não um array ou primitivo. |
| METADATA_TOO_LARGE | 400 | A metadata serializada é maior que 4096 caracteres. |
| IDEMPOTENCY_CONFLICT | 409 | Idempotency-Key foi reutilizado com um valor, moeda ou item diferente. |
| INVOICE_CONFLICT | 409 | invoiceId foi reutilizado com um valor, moeda ou item diferente. |
| INVALID_IMAGE_URL | 400 | imageUrl deve ser uma URL https. |
| ITEM_NAME_REQUIRED | 400 | itemName (ou título) é obrigatório quando items é omitido. |
| INVALID_ITEMS | 400 | items deve ser um array não vazio de itens bloqueados (máx. 20). Cada linha precisa de nome e amount. |
| TOO_MANY_ITEMS | 400 | itens não podem conter mais de 20 linhas. |
| AMOUNT_MISMATCH | 400 | a quantidade deve ser igual à soma de cada valor da linha multiplicado pela quantidade. |
| INVALID_DELIVERY | 400 | Os campos de entrega são inválidos. Cada campo precisa de um nome (máx. 80) e valor (máx. 2048). type deve ser text, password ou textarea (padrão text). Máx. 16 campos por linha. |
| ITEMS_TOO_LARGE | 400 | O JSON dos itens serializados é maior que 48.000 caracteres. |
| NOT_FOUND | 404 | Nenhuma sessão de checkout hospedado corresponde a esse uid ou invoiceId para este vendedor. |
| RESERVE_FAILED | 400 | Webhook de reserva expirou, retornou dados inválidos ou faltaram campos obrigatórios. |
| OPTION_AMBIGUOUS | 409 | Mais de uma opção de preço corresponde a esse nome. Passe optionId do GET stock. |
| OPTION_NOT_FOUND | 404 | Nenhuma opção de preço corresponde a esse id ou nome nesta listagem. |
| OPTION_REQUIRED | 400 | Esta listagem tem várias opções de preço. Passe option ou optionId. |
| UNKNOWN_FIELD | 400 | Um nome de campo não corresponde ao esquema de entrega desta listagem. |
| FIELD_MAPPING_AMBIGUOUS | 400 | Não foi possível mapear colunas ou chaves para os campos de entrega. Envie cabeçalhos ou use objetos de itens indexados pelo nome do campo. |
| STOCK_MODE_MISMATCH | 400 | Esse payload não corresponde ao modo de estoque da opção (quantidade vs itens salvos). |
| IMPORT_TOO_LARGE | 400 | Uma solicitação de reabastecimento pode importar no máximo 1.000 itens salvos por opção. |
| OPTION_ITEM_CAPACITY | 400 | Esta opção de preço já possui o máximo de 5.000 itens salvos não vendidos. |
| DUPLICATE_ITEMS | 409 | onDuplicate=error e pelo menos um item já existe nesta opção. |
| UNLIMITED_STOCK | 400 | Esta opção tem quantidade ilimitada. Use set para mudar para uma contagem finita primeiro. |
| INSUFFICIENT_STOCK | 400 | Quantidade de estoque insuficiente para remover. |
| STOCK_HELD_IN_CHECKOUT | 400 | Não é possível reduzir a quantidade abaixo das unidades atualmente reservadas no checkout. |
| INVALID_RESTOCK | 400 | O corpo do reabastecimento está faltando uma ação obrigatória ou combina add/items em uma opção. |
Lidar com 429
Aguarde usando Retry-After segundos. Não rotacione chaves para contornar limites; o limite é por chave e fixo para todos os vendedores.
Respostas bem-sucedidas incluem X-RateLimit-Limit, X-RateLimit-Remaining e X-RateLimit-Reset.
Crie uma chave nas configurações de Desenvolvedor e conecte o Discord ou Telegram em Notificações.