/api/v1Documento de descubrimiento
Devuelve ámbitos, cuotas, eventos de webhook y el catálogo completo de operaciones. Cualquier clave de API válida funciona.
La API Abierta para Vendedores es para vendedores que quieren alertas de Discord, sincronización de stock, automatización al estilo Zapier, o una oficina trasera personalizada sobre RMT.GG.
Crea una clave API en la configuración de Desarrollador, luego llama a discovery para imprimir el catálogo en vivo.
/api/v1Devuelve ámbitos, cuotas, eventos de webhook y el catálogo completo de operaciones. Cualquier clave de API válida funciona.
Envía tu clave secreta en vivo en cada solicitud a /api/v1. Prefiere solo HTTPS. Nunca incrustes claves en clientes públicos o paquetes de navegador.
Encabezado preferido
Authorization: Bearer rmt_sk_live_<prefix>_<secret>Encabezado alternativo
X-Api-Key: rmt_sk_live_<prefix>_<secret>Cliente TypeScript reutilizable (autenticación Bearer, errores tipados, reintento 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));
}
}
}
Rotar en caso de filtración
Si una clave se filtra, revócala en la configuración de Desarrollador y crea una nueva. Actualiza tu automatización antes de revocar si estás en vivo.
Cada clave de API tiene ámbitos que restringen los puntos finales. Un ámbito faltante devuelve 403 SCOPE_MISSING.
offers:read: Listar y obtener tus ofertas.offers:write: Crear, actualizar, publicar y eliminar ofertas.orders:read: Listar y obtener pedidos de vendedores.orders:write: Marcar pedidos como entregados.webhooks:manage: Reservado para la gestión futura de webhooks de Open API. Configura Discord/Telegram en Notificaciones y webhooks JSON en la configuración de Desarrollador hoy.checkout:write: Crear y leer sesiones de pago alojado. Requiere aprobación de administrador para el pago de socios.Ámbitos de clave predeterminados
Las nuevas claves reciben offers:read, offers:write, orders:read y orders:write. El CRUD de webhooks salientes permanece en la interfaz de usuario de Configuración (autenticación de sesión).
Los identificadores de ofertas aceptan el slug de URL pública o el id numérico. Las respuestas omiten el id interno y sellerId.
Lo que PATCH no puede cambiar aún
Las filas de stock, precios de opciones, medios y atributos se gestionan en el editor de vendedores (o futuros puntos finales), no a través de PATCH hoy.
/api/v1/offersFiltra con archive=active (predeterminado), archived o all.
Solicitud
archive| Nombre | En | Tipo | Requerido | Descripción |
|---|---|---|---|---|
archive | query | string | Opcional | One of "active" (default), "archived", or "all". |
/api/v1/offersCrea un borrador vacío propiedad del vendedor autenticado. No se requiere cuerpo.
/api/v1/offers/:urlOrIdCarga por slug de URL pública o id numérico. Las relaciones (opciones) pueden incluirse; los artículos de stock no.
Solicitud
urlOrId| Nombre | En | Tipo | Requerido | Descripción |
|---|---|---|---|---|
urlOrId | path | string | requerido | Offer.url slug or Offer.id. |
/api/v1/offers/:urlOrIdParchea un subconjunto seguro de campos de listado. Emite offer.updated cuando los webhooks salientes están configurados.
Solicitud
urlOrIdtitledescriptionvisibilitycategoryIdofferingIdthumbnailofferTypelistingMode| Nombre | En | Tipo | Requerido | Descripción |
|---|---|---|---|---|
urlOrId | path | string | requerido | 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/:urlOrIdLas mismas reglas de eliminación/archivo que la interfaz de usuario del vendedor.
Solicitud
urlOrId| Nombre | En | Tipo | Requerido | Descripción |
|---|---|---|---|---|
urlOrId | path | string | requerido | Offer.url slug or Offer.id. |
/api/v1/offers/:urlOrId/publishPublica un borrador (o cambia la visibilidad). Falla con 400 si los campos de listado requeridos están incompletos.
Solicitud
urlOrIdvisibility| Nombre | En | Tipo | Requerido | Descripción |
|---|---|---|---|---|
urlOrId | path | string | requerido | Offer.url slug or Offer.id. |
visibility | body | string | Opcional | Optional. PUBLIC (default), PRIVATE, or UNPUBLISHED. |
Consulta las cantidades comprables por nivel, empareja los nombres de los campos de entrega con la lista correcta, y luego reabastece la cantidad o las claves y cuentas guardadas.
Cómo funciona el emparejamiento
GET /api/v1/stock?fields=username,password encuentra listados cuyo esquema tiene esos campos. Reabastece con nombres de opción (o optionId) y nombres de campo. No necesitas los ids de campo internos. Las respuestas nunca incluyen valores de credenciales.
/api/v1/stockDevuelve cantidades por nivel y nombres de campos de entrega para que puedas emparejar claves y cuentas con la oferta correcta. Filtra con q, fields, stockMode y lowStock. Nunca devuelve valores de credenciales.
Solicitud
qfieldsstockModelowStockarchive| Nombre | En | Tipo | Requerido | Límites | Descripción |
|---|---|---|---|---|---|
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/stockMisma forma de StockOffer que el índice, para una url o id numérico. Solo cantidades.
Solicitud
urlOrId| Nombre | En | Tipo | Requerido | Descripción |
|---|---|---|---|---|
urlOrId | path | string | requerido | Offer.url slug or Offer.id. |
/api/v1/offers/:urlOrId/stockNiveles de cantidad: agregar, eliminar o establecer. Niveles de ítems guardados: objetos de ítems por nombre de campo, keys[] cuando hay un campo, o texto delimitado. Varios niveles en una llamada a través de options[]. dryRun previsualiza el emparejamiento. onDuplicate por defecto se salta.
{
"option": "1 Month",
"add": 50
}Solicitud
urlOrIdoptionoptionIdaddremovesetitemskeystextdelimiterheadersoptionsdryRunonDuplicate| Nombre | En | Tipo | Requerido | Límites | Descripción |
|---|---|---|---|---|---|
urlOrId | path | string | requerido | 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. |
Usa POST /api/v1/offers/:url/stock con items[] para cuentas o keys[] para códigos de licencia de un solo campo. Divide en 1,000 filas por solicitud.
Selecciona la carga útil que coincide con la oferta
Llama primero a GET stock. Si fields[] tiene más de un nombre, envía objetos items con esas claves (Nombre de usuario, Contraseña, Correo electrónico). Si hay exactamente un campo, keys[] es suficiente. Las listas de cantidad usan add, no items.
1,000 filas por solicitud. 5,000 artículos no vendidos por nivel. 300 solicitudes por minuto. Los duplicados se omiten por defecto.
accounts.json (un objeto por cuenta)
[
{ "Username": "player1", "Password": "secret1", "E-Mail": "[email protected]" },
{ "Username": "player2", "Password": "secret2", "E-Mail": "[email protected]" }
]cuentas.csv
Username,Password,E-Mail
player1,secret1,p1@example.com
player2,secret2,p2@example.comkeys.txt (una clave de licencia por línea)
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]"}'Importación por lotes de TypeScript (1,000 filas por solicitud)
// 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) }),
}),
);
}
}
Lo que esta API protege
Las claves necesitan offers:write, tienen límite de tasa, y solo pueden reabastecer tus propias ofertas. GET nunca devuelve credenciales guardadas. Las respuestas de POST no repiten valores de Nombre de usuario, Contraseña o clave. Envía el cuerpo a través de HTTPS en producción y guarda la clave API en una variable de entorno.
En Windows, usa curl.exe (no el alias de curl). Cita el -d JSON para que PowerShell no lo divida.
Los pedidos están restringidos a tu cuenta de vendedor. Los detalles de facturación del comprador pueden ser redactados bajo las reglas de privacidad del mercado de registro.
/api/v1/ordersSoporta limit, offset, status, q y sort (nuevo, viejo, total_alto, total_bajo).
Solicitud
limitoffsetstatusqsort| Nombre | En | Tipo | Requerido | Límites | Descripción |
|---|---|---|---|---|---|
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/:uidDevuelve el pedido con artículos. Usa el uid de pedido público.
Solicitud
uid| Nombre | En | Tipo | Requerido | Descripción |
|---|---|---|---|---|
uid | path | string | requerido | Order.uid. |
/api/v1/orders/:uid/deliverCumplimiento manual. Las líneas COMPLEJAS deben estar completamente adjuntas cuando se requiera. Emite order.delivered.
Solicitud
uidevidence| Nombre | En | Tipo | Requerido | Límites | Descripción |
|---|---|---|---|---|---|
uid | path | string | requerido | Order.uid. | |
evidence | body | string[] | Opcional | HTTPS, max 10 | Optional screenshot or transfer-proof URLs. |
Cualquier tienda asociada aprobada o backend puede enviar compradores a una página de pago de RMT.GG. Nosotros seguimos siendo el comerciante registrado y tomamos un 4% del monto bloqueado.
Lista blanca y cumplimiento
Aplica en Configuración, Checkout alojado, luego crea una clave API y un webhook JSON allí. Después del pago, emitimos checkout.completed. Los valores de entrega permanecen en la confirmación de RMT.GG; no están en el GET del vendedor ni en los webhooks.
/api/v1/checkout/sessionsEnvía a los compradores a una página de pago bloqueada de RMT.GG. Un artículo: cantidad y itemName. Carrito: items[] con nombre y cantidad en cada línea. La moneda predeterminada es USD. Después del pago, el comprador permanece en RMT.GG cuando hay campos de entrega para copiar. returnUrl continúa hacia la tienda; sin entrega, los enviamos de vuelta después de una breve cuenta regresiva. La cantidad, longitudes y otros límites están en la columna de Límites.
{
amount: 10, // what the buyer pays
itemName: "Gold pack", // pay page heading
}Solicitud
amountcurrencyitemNametitledescriptionimageUrlitemsitems[].nameitems[].titleitems[].descriptionitems[].amountitems[].quantityitems[].imageUrlitems[].deliveryitems[].delivery[].nameitems[].delivery[].typeitems[].delivery[].valueemailreturnUrlcancelUrlinvoiceIdcategorySlugofferingmetadataIdempotency-Key| Nombre | En | Tipo | Requerido | Límites | Descripción |
|---|---|---|---|---|---|
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 | requerido | 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 | requerido | > 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 | requerido | 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 | requerido | 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. |
Respuesta
uidstatuspaidamountcurrencyitemNamedescriptionemaillangreturnUrlcancelUrlinvoiceIdexternalInvoiceIdsourceexpiresAthostedUrlorderUiditemshosted_urlexpires_at| Nombre | En | Tipo | Descripción |
|---|---|---|---|
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/:uidDevuelve la sesión que creaste. Usa esto si checkout.completed se retrasa. paid es verdadero solo cuando el estado está pagado. los ítems nunca incluyen valores de entrega.
Solicitud
uid| Nombre | En | Tipo | Requerido | Descripción |
|---|---|---|---|---|
uid | path | string | requerido | Session uid returned at create time. |
Respuesta
uidstatuspaidamountcurrencyitemNamedescriptionemaillangreturnUrlcancelUrlinvoiceIdexternalInvoiceIdsourceexpiresAthostedUrlorderUiditems| Nombre | En | Tipo | Descripción |
|---|---|---|---|
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/sessionsEl mismo objeto de sesión que GET por uid. Pasa el invoiceId que enviaste al crear. Faltante: 400 INVOICE_ID_REQUIRED. Desconocido: 404 NOT_FOUND.
Solicitud
invoiceId| Nombre | En | Tipo | Requerido | Límites | Descripción |
|---|---|---|---|---|---|
invoiceId | query | string | requerido | Max 128 | invoiceId from create. Missing: 400 INVOICE_ID_REQUIRED. Too long: 400 INVALID_INVOICE_ID. |
Respuesta
uidstatuspaidamountcurrencyitemNamedescriptionemaillangreturnUrlcancelUrlinvoiceIdexternalInvoiceIdsourceexpiresAthostedUrlorderUiditems| Nombre | En | Tipo | Descripción |
|---|---|---|---|
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. |
Configura puntos finales HTTPS (o webhooks de Discord) en Configuración → Desarrollador. RMT envía POST cuando se activan eventos suscritos.
Sobre 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 */ ]
}
}
}Encabezados de entrega firmados
{
"X-RMT-Event": "order.paid",
"X-RMT-Delivery": "whd_…",
"X-RMT-Timestamp": "1710000000",
"X-RMT-Signature": "v1=abc123…"
}carga útil de checkout.completado
{
"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 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"
}
]
}
}
}Cuando se establece un secreto de firma, calcula HMAC-SHA256 sobre timestamp + '.' + rawBody y compáralo con el hex después de v1=.
El secreto permanece en RMT. Cada POST firmado incluye X-RMT-Timestamp (segundos Unix) y X-RMT-Signature (v1= más hex). Calcula HMAC-SHA256 sobre la cadena timestamp + '.' + rawBody usando tu secreto, luego compara con el hex después de v1=. Rechaza timestamps más antiguos de 5 minutos.
Verificación TypeScript (comparación segura en tiempo y ventana de repetición 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();
Manejador 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 listados COMPLEJOS (unidad única), RMT puede POSTear tu punto final HTTPS después del pago para crear la siguiente licencia, cuenta o clave cuando el stock local esté corto.
Fallos seguros de pago
Si tu punto final se agota o devuelve datos inválidos, el pedido permanece PAGADO. Se cobra al comprador; ves un error en el pedido y puedes reintentar la reserva o adjuntar claves manualmente.
Cuerpo 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
}Encabezados de solicitud (cuando se establece un secreto de firma)
{
"Content-Type": "application/json",
"X-RMT-Event": "reserve.item",
"X-RMT-Delivery": "rsv_…",
"X-RMT-Timestamp": "1710000000",
"X-RMT-Signature": "v1=abc123…"
}Respuesta de conveniencia
{
"entries": [
{ "name": "License", "value": "AAAA-BBBB-CCCC" }
]
}Campos JSON mapeados (con rutas responseMap como $.license)
{
"license": "AAAA-BBBB-CCCC",
"email": "[email protected]",
"password": "temporary-pass"
}Si estableces un secreto en la oferta, cada POST de reserva está firmado. Recalcula HMAC-SHA256(secreto, timestamp + '.' + rawBody) y compara con X-RMT-Signature después de quitar el prefijo v1=. El secreto en sí nunca se incluye en la solicitud.
Ver ejemplo completo de verificaciónManejador de reserva TypeScript (verificar, luego devolver 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 }],
});
}
No llames a la reserva antes del pago
RMT solo llama a tu punto final después de que el pago tenga éxito, por lo que los checkouts abandonados no queman licencias.
Los errores devuelven JSON { error, code? }. El tráfico de la API Abierta está limitado a 300 solicitudes por minuto por clave de API.
API_KEY_REQUIREDFalta el encabezado Authorization o X-Api-Key.
API_KEY_INVALIDClave desconocida, revocada, expirada o acceso de desarrollador suspendido.
SCOPE_MISSINGLa clave carece del ámbito requerido por el punto final.
RATE_LIMITEDDemasiadas solicitudes. Respeta Retry-After y X-RateLimit-Reset.
CHECKOUT_PARTNER_NOT_APPROVEDEste vendedor no está aprobado para el checkout alojado.
INVALID_JSONEl cuerpo de la solicitud debe ser JSON.
INVALID_AMOUNTla cantidad debe ser mayor que 0 y como máximo 1,000,000.
UNSUPPORTED_CURRENCYla moneda no es un código ISO soportado.
INVALID_RETURN_URLreturnUrl y cancelUrl deben ser https (http://localhost está permitido para tiendas locales).
INVALID_PSP_CATEGORYcategorySlug y offering deben enviarse juntos y coincidir con un par del catálogo.
INVALID_INVOICE_IDinvoiceId tiene más de 128 caracteres.
INVOICE_ID_REQUIREDGET /checkout/sessions requiere invoiceId como parámetro de consulta.
INVALID_IDEMPOTENCY_KEYIdempotency-Key tiene más de 128 caracteres.
INVALID_METADATAmetadata debe ser un objeto JSON, no un array o primitivo.
METADATA_TOO_LARGELa metadata serializada es más grande que 4096 caracteres.
IDEMPOTENCY_CONFLICTIdempotency-Key fue reutilizado con una cantidad, moneda o ítem diferente.
INVOICE_CONFLICTinvoiceId fue reutilizado con una cantidad, moneda o ítem diferente.
INVALID_IMAGE_URLimageUrl debe ser una URL https.
ITEM_NAME_REQUIREDitemName (o título) es obligatorio cuando se omiten los ítems.
INVALID_ITEMSitems debe ser un array no vacío de líneas bloqueadas (máx. 20). Cada línea necesita nombre y cantidad.
TOO_MANY_ITEMSitems no puede contener más de 20 líneas.
AMOUNT_MISMATCHla cantidad debe ser igual a la suma de cada cantidad de línea multiplicada por la cantidad.
INVALID_DELIVERYLos campos de entrega son inválidos. Cada campo necesita un nombre (máx. 80) y un valor (máx. 2048). type debe ser text, password o textarea (por defecto text). Máx. 16 campos por línea.
ITEMS_TOO_LARGEEl JSON de los ítems serializados es más grande que 48,000 caracteres.
NOT_FOUNDNo hay ninguna sesión de pago alojado que coincida con ese uid o invoiceId para este vendedor.
RESERVE_FAILEDEl webhook de reserva se agotó, devolvió datos inválidos o faltaron campos requeridos.
OPTION_AMBIGUOUSMás de una opción de precio coincide con ese nombre. Pasa optionId desde GET stock.
OPTION_NOT_FOUNDNo hay opción de precio que coincida con ese id o nombre en este listado.
OPTION_REQUIREDEste listado tiene múltiples opciones de precio. Pasa option o optionId.
UNKNOWN_FIELDUn nombre de campo no coincide con el esquema de entrega de este listado.
FIELD_MAPPING_AMBIGUOUSNo se pudieron mapear columnas o claves a los campos de entrega. Envía encabezados, o usa objetos de ítems con clave por nombre de campo.
STOCK_MODE_MISMATCHEse payload no coincide con el modo de inventario de la opción (cantidad vs ítems guardados).
IMPORT_TOO_LARGEUna solicitud de reabastecimiento puede importar como máximo 1,000 ítems guardados por opción.
OPTION_ITEM_CAPACITYEsta opción de precio ya tiene el máximo de 5,000 ítems guardados no vendidos.
DUPLICATE_ITEMSonDuplicate=error y al menos un ítem ya existe en esta opción.
UNLIMITED_STOCKEsta opción tiene cantidad ilimitada. Usa set para cambiar a un conteo finito primero.
INSUFFICIENT_STOCKNo hay suficiente cantidad de inventario para eliminar.
STOCK_HELD_IN_CHECKOUTNo se puede reducir la cantidad por debajo de las unidades actualmente reservadas en el carrito.
INVALID_RESTOCKEl cuerpo de reabastecimiento falta una acción requerida, o combina add/items en una opción.
| Código | HTTP | Descripción |
|---|---|---|
| API_KEY_REQUIRED | 401 | Falta el encabezado Authorization o X-Api-Key. |
| API_KEY_INVALID | 401 | Clave desconocida, revocada, expirada o acceso de desarrollador suspendido. |
| SCOPE_MISSING | 403 | La clave carece del ámbito requerido por el punto final. |
| RATE_LIMITED | 429 | Demasiadas solicitudes. Respeta Retry-After y X-RateLimit-Reset. |
| CHECKOUT_PARTNER_NOT_APPROVED | 403 | Este vendedor no está aprobado para el checkout alojado. |
| INVALID_JSON | 400 | El cuerpo de la solicitud debe ser JSON. |
| INVALID_AMOUNT | 400 | la cantidad debe ser mayor que 0 y como máximo 1,000,000. |
| UNSUPPORTED_CURRENCY | 400 | la moneda no es un código ISO soportado. |
| INVALID_RETURN_URL | 400 | returnUrl y cancelUrl deben ser https (http://localhost está permitido para tiendas locales). |
| INVALID_PSP_CATEGORY | 400 | categorySlug y offering deben enviarse juntos y coincidir con un par del catálogo. |
| INVALID_INVOICE_ID | 400 | invoiceId tiene más de 128 caracteres. |
| INVOICE_ID_REQUIRED | 400 | GET /checkout/sessions requiere invoiceId como parámetro de consulta. |
| INVALID_IDEMPOTENCY_KEY | 400 | Idempotency-Key tiene más de 128 caracteres. |
| INVALID_METADATA | 400 | metadata debe ser un objeto JSON, no un array o primitivo. |
| METADATA_TOO_LARGE | 400 | La metadata serializada es más grande que 4096 caracteres. |
| IDEMPOTENCY_CONFLICT | 409 | Idempotency-Key fue reutilizado con una cantidad, moneda o ítem diferente. |
| INVOICE_CONFLICT | 409 | invoiceId fue reutilizado con una cantidad, moneda o ítem diferente. |
| INVALID_IMAGE_URL | 400 | imageUrl debe ser una URL https. |
| ITEM_NAME_REQUIRED | 400 | itemName (o título) es obligatorio cuando se omiten los ítems. |
| INVALID_ITEMS | 400 | items debe ser un array no vacío de líneas bloqueadas (máx. 20). Cada línea necesita nombre y cantidad. |
| TOO_MANY_ITEMS | 400 | items no puede contener más de 20 líneas. |
| AMOUNT_MISMATCH | 400 | la cantidad debe ser igual a la suma de cada cantidad de línea multiplicada por la cantidad. |
| INVALID_DELIVERY | 400 | Los campos de entrega son inválidos. Cada campo necesita un nombre (máx. 80) y un valor (máx. 2048). type debe ser text, password o textarea (por defecto text). Máx. 16 campos por línea. |
| ITEMS_TOO_LARGE | 400 | El JSON de los ítems serializados es más grande que 48,000 caracteres. |
| NOT_FOUND | 404 | No hay ninguna sesión de pago alojado que coincida con ese uid o invoiceId para este vendedor. |
| RESERVE_FAILED | 400 | El webhook de reserva se agotó, devolvió datos inválidos o faltaron campos requeridos. |
| OPTION_AMBIGUOUS | 409 | Más de una opción de precio coincide con ese nombre. Pasa optionId desde GET stock. |
| OPTION_NOT_FOUND | 404 | No hay opción de precio que coincida con ese id o nombre en este listado. |
| OPTION_REQUIRED | 400 | Este listado tiene múltiples opciones de precio. Pasa option o optionId. |
| UNKNOWN_FIELD | 400 | Un nombre de campo no coincide con el esquema de entrega de este listado. |
| FIELD_MAPPING_AMBIGUOUS | 400 | No se pudieron mapear columnas o claves a los campos de entrega. Envía encabezados, o usa objetos de ítems con clave por nombre de campo. |
| STOCK_MODE_MISMATCH | 400 | Ese payload no coincide con el modo de inventario de la opción (cantidad vs ítems guardados). |
| IMPORT_TOO_LARGE | 400 | Una solicitud de reabastecimiento puede importar como máximo 1,000 ítems guardados por opción. |
| OPTION_ITEM_CAPACITY | 400 | Esta opción de precio ya tiene el máximo de 5,000 ítems guardados no vendidos. |
| DUPLICATE_ITEMS | 409 | onDuplicate=error y al menos un ítem ya existe en esta opción. |
| UNLIMITED_STOCK | 400 | Esta opción tiene cantidad ilimitada. Usa set para cambiar a un conteo finito primero. |
| INSUFFICIENT_STOCK | 400 | No hay suficiente cantidad de inventario para eliminar. |
| STOCK_HELD_IN_CHECKOUT | 400 | No se puede reducir la cantidad por debajo de las unidades actualmente reservadas en el carrito. |
| INVALID_RESTOCK | 400 | El cuerpo de reabastecimiento falta una acción requerida, o combina add/items en una opción. |
Manejar 429
Retrocede usando Retry-After segundos. No gires claves para eludir límites; el límite es por clave y es plano para todos los vendedores.
Las respuestas exitosas incluyen X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset.
Crea una clave en la configuración de Desarrollador y conecta Discord o Telegram en Notificaciones.