RMT.GG/Documentación para desarrolladores vendedores
v1

API para vendedores

Automatiza listados, cumple ventas y transmite eventos de pedidos. Incluye webhooks salientes y puntos finales de reserva para reabastecimiento de inventario a demanda después del pago.

API REST Abierta

Autenticación Bearer en /api/v1 para ofertas y pedidos, con encabezados de descubrimiento y límite de tasa.

Webhooks salientes

Entregas HTTPS (o Discord) firmadas para eventos del ciclo de vida de pedidos y ofertas.

Reserva / reabastecimiento

Crea stock COMPLEJO desde tu servidor después del pago cuando el inventario local esté corto.

Lo que puedes construir

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.

  • Gestionar ofertas
    Crea borradores, actualiza campos seguros, publica y archiva a través de /api/v1/offers.
  • Cumplir ventas
    Lista e inspecciona los pedidos de los vendedores, luego márcalos como entregados con URLs de evidencia opcionales.
  • Mantente dentro del límite
    Cada clave tiene un límite de 300 solicitudes por minuto. Las respuestas incluyen encabezados X-RateLimit-*.
  • Reacciona en tiempo real
    Suscríbete a eventos de pedidos y ofertas, o reabastece el inventario COMPLEJO con webhooks de reserva.
  • Acepta pagos desde tu tienda
    Los socios aprobados pueden enviar compradores desde una tienda externa al pago alojado, y luego cumplir con el pedido.pago.

Inicio rápido

Crea una clave API en la configuración de Desarrollador, luego llama a discovery para imprimir el catálogo en vivo.

  1. 1Abre Configuración → Desarrollador (sin paso de habilitación separado).
  2. 2Crea una clave de API y copia la clave secreta una vez (rmt_sk_live_…). Almacénala en tu gestor de secretos.
  3. 3Llama a GET /api/v1 con Authorization: Bearer para confirmar ámbitos, cuotas y operaciones.
GET/api/v1

Documento de descubrimiento

Devuelve ámbitos, cuotas, eventos de webhook y el catálogo completo de operaciones. Cualquier clave de API válida funciona.

Autenticación

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

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

Encabezado alternativo

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

Cliente TypeScript reutilizable (autenticación Bearer, errores tipados, reintento 429)

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

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

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

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

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

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

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

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.

Ámbitos

Cada clave de API tiene ámbitos que restringen los puntos finales. Un ámbito faltante devuelve 403 SCOPE_MISSING.

offers:read
offers:write
orders:read
orders:write
webhooks:manage
checkout:write
  • 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).

API de Ofertas

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.

GET/api/v1/offers
offers:read

Lista tus ofertas

Filtra con archive=active (predeterminado), archived o all.

Solicitud

  • archive
    En
    query
    Tipo
    string
    Requerido
    Opcional
    Descripción
    One of "active" (default), "archived", or "all".
  • Response: { offers: Offer[], total: number }. Numeric id and sellerId are omitted.
POST/api/v1/offers
offers:write

Crear una oferta en borrador

Crea un borrador vacío propiedad del vendedor autenticado. No se requiere cuerpo.

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

Obtener una oferta

Carga por slug de URL pública o id numérico. Las relaciones (opciones) pueden incluirse; los artículos de stock no.

Solicitud

  • urlOrId
    En
    path
    Tipo
    string
    Requerido
    requerido
    Descripción
    Offer.url slug or Offer.id.
  • Returns relations (options, etc.) when available; items are not included.
PATCH/api/v1/offers/:urlOrId
offers:write

Actualizar campos de oferta

Parchea un subconjunto seguro de campos de listado. Emite offer.updated cuando los webhooks salientes están configurados.

Solicitud

  • urlOrId
    En
    path
    Tipo
    string
    Requerido
    requerido
    Descripción
    Offer.url slug or Offer.id.
  • title
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Descripción
    Listing title.
  • description
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Descripción
    Listing description.
  • visibility
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Descripción
    PUBLIC | PRIVATE | UNPUBLISHED.
  • categoryId
    En
    body
    Tipo
    number
    Requerido
    Opcional
    Descripción
    Catalog category id.
  • offeringId
    En
    body
    Tipo
    number
    Requerido
    Opcional
    Descripción
    Catalog offering id.
  • thumbnail
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Descripción
    Thumbnail URL or asset reference.
  • offerType
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Descripción
    Offer type string used by the listing.
  • listingMode
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Descripción
    Listing mode (for example STANDARD, RANK_BOOST, SESSION).
  • At least one allowed field is required.
  • Emits offer.updated webhook when configured.
  • Stock is managed via GET/POST /api/v1/offers/:urlOrId/stock. Option prices, media, and attributes are not editable via this endpoint yet.
DELETE/api/v1/offers/:urlOrId
offers:write

Eliminar o archivar

Las mismas reglas de eliminación/archivo que la interfaz de usuario del vendedor.

Solicitud

  • urlOrId
    En
    path
    Tipo
    string
    Requerido
    requerido
    Descripción
    Offer.url slug or Offer.id.
  • Response: { ok: true }.
POST/api/v1/offers/:urlOrId/publish
offers:write

Publicar una oferta

Publica un borrador (o cambia la visibilidad). Falla con 400 si los campos de listado requeridos están incompletos.

Solicitud

  • urlOrId
    En
    path
    Tipo
    string
    Requerido
    requerido
    Descripción
    Offer.url slug or Offer.id.
  • visibility
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Descripción
    Optional. PUBLIC (default), PRIVATE, or UNPUBLISHED.
  • Response: { offer: Offer }.
  • Fails if the listing is incomplete for publish.

API de Inventario

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.

GET/api/v1/stock
offers:read

Listar inventario en tus listados

Devuelve 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

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

Obtener inventario para un listado

Misma forma de StockOffer que el índice, para una url o id numérico. Solo cantidades.

Solicitud

  • urlOrId
    En
    path
    Tipo
    string
    Requerido
    requerido
    Descripción
    Offer.url slug or Offer.id.
  • Response: { offer } with the same StockOffer shape as GET /api/v1/stock.
  • Counts only. Use the seller editor to inspect saved key values.
POST/api/v1/offers/:urlOrId/stock
offers:write

Reabastecer un listado

Niveles 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.

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

Solicitud

  • urlOrId
    En
    path
    Tipo
    string
    Requerido
    requerido
    Descripción
    Offer.url slug or Offer.id.
  • option
    En
    body
    Tipo
    string
    Requerido
    Condicional
    Descripción
    Pricing option name (case-insensitive). Omit when the listing has a single tier.
  • optionId
    En
    body
    Tipo
    number
    Requerido
    Condicional
    Descripción
    Pricing option id from GET stock. Wins over option when both are sent. Ambiguous names return 409 OPTION_AMBIGUOUS.
  • add
    En
    body
    Tipo
    number
    Requerido
    Condicional
    Límites
    1-1,000,000
    Descripción
    QUANTITY: add this many units. Fails with 400 UNLIMITED_STOCK if the tier is unlimited.
  • remove
    En
    body
    Tipo
    number
    Requerido
    Condicional
    Límites
    1-1,000,000
    Descripción
    QUANTITY: withdraw this many units. Fails with 400 INSUFFICIENT_STOCK when there is not enough.
  • set
    En
    body
    Tipo
    number | null
    Requerido
    Condicional
    Descripción
    QUANTITY: set an absolute count. null means unlimited. Cannot go below units held in checkout.
  • items
    En
    body
    Tipo
    object[]
    Requerido
    Condicional
    Límites
    Max 1,000
    Descripción
    COMPLEX: objects keyed by delivery field name, for example { "Username": "a", "Password": "b" }. Names match case-insensitively.
  • keys
    En
    body
    Tipo
    string[]
    Requerido
    Condicional
    Límites
    Max 1,000
    Descripción
    COMPLEX: license keys when the listing has exactly one delivery field. Otherwise 400 FIELD_MAPPING_AMBIGUOUS.
  • text
    En
    body
    Tipo
    string
    Requerido
    Condicional
    Límites
    Max 1,000 rows
    Descripción
    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
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Límites
    Default :
    Descripción
    Delimiter for text. Ignored unless text is sent.
  • headers
    En
    body
    Tipo
    string[]
    Requerido
    Opcional
    Descripción
    Optional column headers for text when the first line is data, not names.
  • options
    En
    body
    Tipo
    object[]
    Requerido
    Condicional
    Descripción
    Restock several tiers in one call. Each element is the same shape as a single-option body (option, add, items, …).
  • dryRun
    En
    body
    Tipo
    boolean
    Requerido
    Opcional
    Descripción
    Preview matching and counts without writing. Default false.
  • onDuplicate
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Límites
    skip (default) or error
    Descripción
    COMPLEX: skip existing unsold fingerprints, or fail the request with 409 DUPLICATE_ITEMS.
  • Send exactly one action per option: add, remove, set, items, keys, or text.
  • Sending items to a QUANTITY tier (or add to a COMPLEX tier) returns 400 STOCK_MODE_MISMATCH.
  • Responses never echo credential values. COMPLEX results include imported, skippedDuplicates, errors, and matchedFields.
  • Saved items are capped at 5,000 unsold rows per option. A single request may import at most 1,000 rows.

Importar muchas cuentas o claves

Guía

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.

  1. 1OBTÉN la oferta. Usa fields[] y stockMode para elegir artículos, claves o añadir.
  2. 2Guarda las cuentas como JSON o CSV usando el nombre del campo. Guarda las claves de licencia una por línea.
  3. 3Haz una prueba primero. Verifica wouldImport, skippedDuplicates y matchedFields.
  4. 4Vuelve a enviar el mismo cuerpo sin dryRun para escribir el stock.

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)

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

cuentas.csv

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

keys.txt (una clave de licencia por línea)

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

cURL

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

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

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

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

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

Importación por lotes de TypeScript (1,000 filas por solicitud)

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

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

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

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.

API de Pedidos

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.

GET/api/v1/orders
orders:read

Lista de pedidos de vendedores

Soporta limit, offset, status, q y sort (nuevo, viejo, total_alto, total_bajo).

Solicitud

  • limit
    En
    query
    Tipo
    number
    Requerido
    Opcional
    Límites
    1-100, default 20
    Descripción
    Page size.
  • offset
    En
    query
    Tipo
    number
    Requerido
    Opcional
    Límites
    >= 0, default 0
    Descripción
    Skip this many rows.
  • status
    En
    query
    Tipo
    string
    Requerido
    Opcional
    Límites
    Max 32
    Descripción
    Filter by order status (for example PAID, DELIVERED, COMPLETED).
  • q
    En
    query
    Tipo
    string
    Requerido
    Opcional
    Límites
    Max 80
    Descripción
    Search reference or related text.
  • sort
    En
    query
    Tipo
    string
    Requerido
    Opcional
    Límites
    newest (default)
    Descripción
    newest | oldest | total_high | total_low.
  • Response: { orders: Order[], total: number }.
GET/api/v1/orders/:uid
orders:read

Obtener un pedido

Devuelve el pedido con artículos. Usa el uid de pedido público.

Solicitud

  • uid
    En
    path
    Tipo
    string
    Requerido
    requerido
    Descripción
    Order.uid.
  • Response: { order } with line items.
  • Buyer billing fields may be redacted under marketplace-of-record privacy rules.
POST/api/v1/orders/:uid/deliver
orders:write

Marcar como entregado

Cumplimiento manual. Las líneas COMPLEJAS deben estar completamente adjuntas cuando se requiera. Emite order.delivered.

Solicitud

  • uid
    En
    path
    Tipo
    string
    Requerido
    requerido
    Descripción
    Order.uid.
  • evidence
    En
    body
    Tipo
    string[]
    Requerido
    Opcional
    Límites
    HTTPS, max 10
    Descripción
    Optional screenshot or transfer-proof URLs.
  • Response: { success: true, order }.
  • COMPLEX inventory lines must be fully attached before deliver when the product requires it.
  • Emits order.delivered webhook when configured.

Pago alojado

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.

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

Crear una sesión de pago alojado

Enví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.

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

Solicitud

  • amount
    En
    body
    Tipo
    number
    Requerido
    Condicional
    Límites
    > 0, max 1,000,000
    Descripción
    What the buyer pays. Required for a single item. With items[], omit it or send the line sum. Mismatch: 400 AMOUNT_MISMATCH.
  • currency
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Límites
    Default USD
    Descripción
    ISO 4217 code such as USD or EUR.
  • itemName
    En
    body
    Tipo
    string
    Requerido
    Condicional
    Límites
    Max 120
    Descripción
    Pay page heading. Required for a single item. Alias: title. With items[], defaults to the first line name. Missing: 400 ITEM_NAME_REQUIRED.
  • title
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Descripción
    Alias of itemName. If both are sent, itemName wins.
  • description
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Límites
    Max 200
    Descripción
    Copy under the heading. If omitted, the heading is reused.
  • imageUrl
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Límites
    HTTPS, max 2048
    Descripción
    Product image, or fallback for lines without imageUrl. Invalid: 400 INVALID_IMAGE_URL.
  • items
    En
    body
    Tipo
    object[]
    Requerido
    Condicional
    Límites
    1-20 lines, JSON max 48,000
    Descripción
    Locked cart. Required when amount is omitted. Buyers cannot change lines. Empty: 400 INVALID_ITEMS.
  • items[].name
    En
    body
    Tipo
    string
    Requerido
    requerido
    Límites
    Max 120
    Descripción
    Line title. Alias: title.
  • items[].title
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Descripción
    Alias of items[].name. If both are sent, name wins.
  • items[].description
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Límites
    Max 200
    Descripción
    Line copy under the name.
  • items[].amount
    En
    body
    Tipo
    number
    Requerido
    requerido
    Límites
    > 0, max 1,000,000
    Descripción
    Unit price. Session total is sum(amount * quantity).
  • items[].quantity
    En
    body
    Tipo
    number
    Requerido
    Opcional
    Límites
    1-99, default 1
    Descripción
    Locked on the pay page.
  • items[].imageUrl
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Límites
    HTTPS, max 2048
    Descripción
    Line image. Falls back to top-level imageUrl.
  • items[].delivery
    En
    body
    Tipo
    object[]
    Requerido
    Opcional
    Límites
    Max 16 fields
    Descripción
    Shown after payment on RMT.GG. Seller GET and webhooks omit values.
  • items[].delivery[].name
    En
    body
    Tipo
    string
    Requerido
    requerido
    Límites
    Max 80
    Descripción
    Field label, for example Code or Password.
  • items[].delivery[].type
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Límites
    text, password, textarea
    Descripción
    password is blurred until the buyer reveals it. Default text.
  • items[].delivery[].value
    En
    body
    Tipo
    string
    Requerido
    requerido
    Límites
    Max 2048
    Descripción
    Field value. Numbers are stored as strings. Empty: 400 INVALID_DELIVERY.
  • email
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Límites
    Invalid values ignored
    Descripción
    Prefills the pay page. The buyer still confirms email before paying.
  • returnUrl
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Límites
    HTTPS, max 2048
    Descripción
    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
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Límites
    HTTPS, max 2048
    Descripción
    Redirect if the buyer cancels or the session expires. If omitted, they stay on the pay page.
  • invoiceId
    En
    body
    Tipo
    string
    Requerido
    Opcional
    Límites
    Max 128
    Descripción
    Your shop id. Same payload returns the existing session. A different payload: 409 INVOICE_CONFLICT.
  • categorySlug
    En
    body
    Tipo
    string
    Requerido
    Condicional
    Límites
    With offering, or omit both
    Descripción
    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
    En
    body
    Tipo
    string
    Requerido
    Condicional
    Límites
    With categorySlug, or omit both
    Descripción
    Catalog offering such as Mods. Mapped to labels like Games · Add-ons.
  • metadata
    En
    body
    Tipo
    object
    Requerido
    Opcional
    Límites
    Object, max 4096 chars
    Descripción
    Stored on the session. Not returned on seller GET.
  • Idempotency-Key
    En
    header
    Tipo
    string
    Requerido
    Opcional
    Límites
    Max 128
    Descripción
    Replay header. Same key and payload returns the existing session. A different payload: 409 IDEMPOTENCY_CONFLICT.

Respuesta

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

Obtener una sesión de pago alojado

Devuelve 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
    En
    path
    Tipo
    string
    Requerido
    requerido
    Descripción
    Session uid returned at create time.

Respuesta

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

Buscar una sesión de pago alojada por ID de factura

El 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
    En
    query
    Tipo
    string
    Requerido
    requerido
    Límites
    Max 128
    Descripción
    invoiceId from create. Missing: 400 INVOICE_ID_REQUIRED. Too long: 400 INVALID_INVOICE_ID.

Respuesta

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

Webhooks salientes

Configura puntos finales HTTPS (o webhooks de Discord) en Configuración → Desarrollador. RMT envía POST cuando se activan eventos suscritos.

order.paid
order.delivered
order.completed
order.refunded
order.disputed
offer.published
offer.updated
checkout.completed
checkout.canceled
checkout.refunded
  • El formato JSON publica un sobre estructurado con id, tipo, creado y datos.
  • El formato Discord publica embeds ricos con enlaces de pedidos u ofertas.
  • La firma opcional utiliza X-RMT-Timestamp y X-RMT-Signature (el mismo esquema que la reserva).
  • El historial de entregas aparece bajo cada punto final para que puedas reintentar fallos. Los puntos finales se pausarán automáticamente después de fallos repetidos.
  • El checkout alojado envía checkout.completed, checkout.canceled y checkout.refunded con data.checkout. Los valores de entrega se omiten. Las ventas del marketplace mantienen order.paid y otros eventos order.*.

Sobre de entrega JSON

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

Encabezados de entrega firmados

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

carga útil de checkout.completado

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

Carga de checkout.canceled

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

Verificar firmas de webhook

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.

  • Lee los bytes del cuerpo en bruto exactamente como se reciben. No analices JSON ni re-serialices antes de hacer el hash.
  • Usa el valor del encabezado X-RMT-Timestamp como prefijo de timestamp (la misma cadena, sin reformatear).
  • Compara con una verificación de igualdad segura en cuanto a tiempo. Rechaza solicitudes con firmas faltantes o no coincidentes cuando se configura un secreto.
  • Rechaza timestamps más antiguos de 5 minutos para limitar la repetición. El mismo esquema se aplica a reserve.item y eventos de pedido o checkout salientes.

Verificación TypeScript (comparación segura en tiempo y ventana de repetición de 5 minutos)

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

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

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

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

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

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

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

Manejador de webhook TypeScript

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

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

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

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

Reservar webhooks (reabastecimiento de inventario)

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.

Cómo configurarlo

  1. Crea una oferta COMPLEJA con campos de ítem (por ejemplo, Licencia).
  2. En el paso de Ítems, habilita el endpoint de inventario bajo demanda y pega tu URL pública HTTPS.
  3. Opcionalmente, establece un secreto de firma para que RMT envíe X-RMT-Timestamp y X-RMT-Signature en cada llamada.
  4. Ejecuta la prueba (o pega JSON de muestra), mapea las rutas de respuesta a los campos de ítem, luego guarda.
  5. Publica la oferta. Los compradores pueden comprar con stock local vacío; las claves se crean después del pago.
  • El stock local siempre es preferido; el webhook solo llena el déficit.
  • Configura un valor predeterminado a nivel de oferta, o anula por opción de precio, en el paso de Items del editor de ofertas.
  • Solo HTTPS. La firma HMAC opcional coincide con los webhooks salientes (X-RMT-Event: reserve.item).
  • Probar en el editor envía dryRun: true. En la página de pedidos, usa Reintentar reserva después de arreglar tu punto final.

Cuerpo POST canónico (truncado)

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

Encabezados de solicitud (cuando se establece un secreto de firma)

json
{
  "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

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

Campos JSON mapeados (con rutas responseMap como $.license)

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

Cómo verificar el secreto de firma

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ón

Manejador de reserva TypeScript (verificar, luego devolver entradas)

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 }],
  });
}

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.

Errores y límites de tasa

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_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.

¿Listo para automatizar?

Crea una clave en la configuración de Desarrollador y conecta Discord o Telegram en Notificaciones.