API REST - v1

API de recargas de juegos y gift cards para revendedores

Una sola API REST para recargas de juegos y códigos de gift card: lea el catálogo, valide al jugador, cree el pedido y reciba el resultado por webhook o consultando el estado.

Esta es la API de recargas host-to-host (H2H) que hay detrás de SHOP2TOPUP. Es una API JSON sobre HTTPS, con autenticación Bearer, claves de idempotencia UUID, límites de solicitudes publicados y códigos de error estables legibles por máquina, de modo que una tienda, un sitio o un bot puede vender crédito de juego de forma programática sin ninguna librería cliente. La misma interfaz ya movió 3,000,000+ pedidos entregados a lo largo de 5+ años de operación continua.

¿Busca precios, medios de pago y cómo funciona la cuenta de socio? Lea la presentación del programa de revendedores de SHOP2TOPUP.

Su primera llamada, antes de firmar nada

Cada endpoint es una solicitud HTTPS común con un único encabezado. No hay SDK que instalar, ni librería cliente que incorporar, ni paso de compilación que agregar. Pegue la llamada de abajo, ponga su clave y tendrá una respuesta del catálogo en vivo en su terminal.

Solicitud - cURL
curl -s "https://shop2topup.com/api/endpoints/v1/catalog/categories?bigCategoryId=1" \
  -H "Authorization: Bearer YOUR_KEY_ID.YOUR_KEY_SECRET"
Respuesta - 200 OK
{
  "success": true,
  "categories": [
    {
      "id": 12,
      "name": "Free Fire",
      "description": "Garena Free Fire diamonds",
      "big_category_id": 1,
      "big_category_name": "Mobile Games"
    }
  ]
}

URL base

https://shop2topup.com/api/endpoints/v1

Todas las rutas documentadas son relativas a esta base. Las respuestas son JSON y siempre llevan un campo booleano success, así que una sola condición en su cliente separa el camino feliz de todo lo demás.

Envoltorio de error

Los fallos mantienen la misma forma en cualquier código de estado. Ramifique según error.code, que forma parte del contrato; nunca según error.message, que está escrito para personas y puede reescribirse en cualquier momento.

Respuesta de error
{
  "success": false,
  "error": {
    "code": "PRICE_INCREASED",
    "message": "Price has increased beyond expected",
    "details": {
      "expected_unit_price": "0.950000",
      "current_unit_price": "0.980000"
    }
  }
}

Toda la superficie de la API en una sola pantalla

Once endpoints cubren la integración completa: leer el catálogo, obtener el precio de un artículo, verificar un jugador, hacer un pedido y conciliarlo después. Cada fila enlaza directo a la referencia completa, con todos los parámetros, una solicitud de ejemplo y una respuesta de ejemplo.

MétodoRutaPara qué sirveReferencia
GET/accountGet Account InfoLeer la referencia
GET/catalog/big-categoriesList Big CategoriesLeer la referencia
GET/catalog/categoriesList CategoriesLeer la referencia
GET/catalog/subcategoriesList Subcategories (Products)Leer la referencia
GET/catalog/subcategory/:itemId/priceGet Item PriceLeer la referencia
GET/catalog/category/:categoryId/requirementsGet Category RequirementsLeer la referencia
POST/player/validateValidate PlayerLeer la referencia
POST/orders/createCreate OrderLeer la referencia
GET/orders/:orderIdGet Order StatusLeer la referencia
POST/orders/batchBatch Get OrdersLeer la referencia
GET/ordersList OrdersLeer la referencia

A esto se suman los webhooks de estado de pedido, documentados en detalle más abajo en esta página.

Autenticación y control de acceso

La autenticación es un único encabezado HTTP. Cada solicitud lleva un encabezado Authorization con una credencial Bearer formada por un ID de clave y un secreto, emitidos desde la página API Access de su panel de revendedor. El ID de clave identifica la cuenta; el secreto demuestra que es suya y se verifica del lado del servidor con una comprobación HMAC.

El secreto se muestra una sola vez, al crearlo, y después ya no se puede recuperar. Guárdelo en el gestor de secretos que su stack ya utiliza, inyéctelo como variable de entorno y manténgalo fuera del control de versiones. Si lo pierde, se rota; no se recupera.

No hay redirección OAuth, ni cookie de sesión, ni refresh token, ni nada que programar. Eso importa en integraciones automatizadas: una tarea cron o un worker de cola puede conservar la misma credencial durante años sin ninguna vía de renovación, y no hay ningún reloj de expiración que lo despierte a las tres de la mañana.

A una clave se le puede asociar una lista de IP permitidas. En cuanto deja de estar vacía, las solicitudes desde cualquier otra dirección se rechazan con IP_NOT_ALLOWED y HTTP 403 antes de ejecutar cualquier lógica de negocio, así que una credencial filtrada es inútil fuera de sus propios servidores. Rotar es un despliegue, no una migración: cree la clave nueva, publíquela y elimine la anterior.

Las claves pertenecen a una sola cuenta de revendedor y gastan la billetera de esa cuenta, así que nunca envíe un secreto a un navegador, a un paquete móvil ni a un repositorio público. Llame a la API desde su backend y deje que su front-end hable con ese backend. Todo lo documentado aquí asume una llamada de servidor a servidor.

Encabezado de la solicitud
Authorization: Bearer <keyId>.<secret>

Verifique una clave en una sola llamada

GET /account es el chequeo de salud de una credencial. Responde tres preguntas a la vez: si la clave autentica, si la cuenta está habilitada y si la billetera alcanza para lo que está por pedir. Ejecútelo al arrancar y antes de cualquier proceso por lotes.

Leer la referencia de autenticación y cuenta

Una integración completa, de punta a punta

Una integración funcionando son cuatro pasos. Los ejemplos de abajo los ejecutan todos contra la API real, en cURL, Node y PHP. Cambie la credencial por la suya y funcionan tal cual están.

  1. 1

    Leer el catálogo

    Recorra las grandes categorías, luego las categorías y luego las subcategorías, y lea el precio actual del artículo que está por vender. Cachee el árbol; consulte el precio en el momento.

  2. 2

    Validar al jugador

    Resuelva el identificador a un nombre dentro del juego y a una región antes de mover dinero. Ahí es donde se atrapan los errores de tipeo y las confusiones de región.

  3. 3

    Crear el pedido

    Genere un UUID, guárdelo y envíelo como order_id. Es su clave de idempotencia, y es la única forma segura de reintentar una solicitud de creación.

  4. 4

    Confirmar la entrega

    Espere el webhook de estado del pedido, y recurra a una lectura de estado para todo lo que siga en pending pasado su propio tiempo de espera.

cURL - flujo completoMostrar código
export S2T_KEY="YOUR_KEY_ID.YOUR_KEY_SECRET"
export S2T_BASE="https://shop2topup.com/api/endpoints/v1"

# 1. Find the item you want to sell.
curl -s "$S2T_BASE/catalog/subcategories?categoryId=12" \
  -H "Authorization: Bearer $S2T_KEY"

# 2. Read the exact price you will be charged for it.
curl -s "$S2T_BASE/catalog/subcategory/999/price" \
  -H "Authorization: Bearer $S2T_KEY"

# 3. Confirm the player exists BEFORE any money moves.
curl -s -X POST "$S2T_BASE/player/validate" \
  -H "Authorization: Bearer $S2T_KEY" \
  -H "Content-Type: application/json" \
  -d '{"sub_category_id": 999, "player_id": "123456789", "server": "Asia"}'

# 4. Create the order. order_id is YOUR uuid and YOUR idempotency key.
curl -s -X POST "$S2T_BASE/orders/create" \
  -H "Authorization: Bearer $S2T_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "order_id": "01912345-6789-7abc-8def-0123456789ab",
    "sub_category_id": 999,
    "quantity": 1,
    "requirements": { "player_id": "123456789", "server": "Asia" },
    "expected_unit_price": "0.950000"
  }'

# 5. Read the order back until it leaves "pending".
curl -s "$S2T_BASE/orders/01912345-6789-7abc-8def-0123456789ab" \
  -H "Authorization: Bearer $S2T_KEY"
Node.js - flujo completoMostrar código
// Node 18+ — no dependencies, global fetch and global crypto.
const BASE = 'https://shop2topup.com/api/endpoints/v1';
const KEY = process.env.S2T_KEY; // "<keyId>.<secret>", server-side only.

async function call(path, init = {}) {
  const res = await fetch(BASE + path, {
    ...init,
    headers: {
      Authorization: `Bearer ${KEY}`,
      'Content-Type': 'application/json',
      ...(init.headers || {}),
    },
  });

  if (res.status === 429) {
    const wait = Number(res.headers.get('Retry-After') || 5);
    await new Promise((r) => setTimeout(r, wait * 1000));
    return call(path, init);
  }

  const body = await res.json();
  if (!res.ok || body.success === false) {
    throw Object.assign(new Error(body?.error?.code || 'HTTP_' + res.status), {
      code: body?.error?.code,
      status: res.status,
    });
  }
  return body;
}

async function sell({ itemId, categoryId, playerId, server }) {
  // 1 + 2. Catalog and current price.
  const { subcategories } = await call(
    `/catalog/subcategories?categoryId=${categoryId}`,
  );
  const item = subcategories.find((s) => s.item_id === itemId);
  const { price } = await call(`/catalog/subcategory/${itemId}/price`);

  // 3. Player check before charging.
  const { player } = await call('/player/validate', {
    method: 'POST',
    body: JSON.stringify({
      sub_category_id: itemId,
      player_id: playerId,
      server,
    }),
  });

  // 4. Create the order. Persist orderId BEFORE the call so a crash mid-flight
  //    can be replayed with the same uuid instead of double-charging.
  const orderId = crypto.randomUUID();
  await saveIntent(orderId, itemId, playerId);

  const { order } = await call('/orders/create', {
    method: 'POST',
    body: JSON.stringify({
      order_id: orderId,
      sub_category_id: itemId,
      quantity: 1,
      requirements: { player_id: playerId, server },
      expected_unit_price: price.unit_price,
    }),
  });

  // 5. Confirm. A webhook usually beats the poll; the poll is the safety net.
  let status = order.status;
  while (status === 'pending') {
    await new Promise((r) => setTimeout(r, 20000));
    status = (await call(`/orders/${orderId}`)).order.status;
  }

  return { orderId, status, itemName: item?.name, playerName: player.player_name };
}
PHP - flujo completoMostrar código
<?php
// PHP 8 — plain cURL, no SDK required.
const S2T_BASE = 'https://shop2topup.com/api/endpoints/v1';

function s2t(string $path, ?array $body = null): array
{
    $ch = curl_init(S2T_BASE . $path);
    $headers = [
        'Authorization: Bearer ' . getenv('S2T_KEY'),
        'Content-Type: application/json',
    ];

    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_TIMEOUT        => 30,
    ]);

    if ($body !== null) {
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
    }

    $raw    = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $data = json_decode($raw, true) ?: [];

    if ($status >= 400 || ($data['success'] ?? false) === false) {
        throw new RuntimeException($data['error']['code'] ?? 'HTTP_' . $status);
    }

    return $data;
}

// 1 + 2. Catalog, then the exact price for the item.
$catalog = s2t('/catalog/subcategories?categoryId=12');
$price   = s2t('/catalog/subcategory/999/price');

// 3. Player check before charging.
$player = s2t('/player/validate', [
    'sub_category_id' => 999,
    'player_id'       => '123456789',
    'server'          => 'Asia',
]);

// 4. Create the order with your own uuid as the idempotency key.
$orderId = sprintf(
    '%04x%04x-%04x-7%03x-%04x-%04x%04x%04x',
    random_int(0, 0xffff), random_int(0, 0xffff),
    random_int(0, 0xffff), random_int(0, 0x0fff),
    random_int(0x8000, 0xbfff),
    random_int(0, 0xffff), random_int(0, 0xffff), random_int(0, 0xffff)
);

$order = s2t('/orders/create', [
    'order_id'            => $orderId,
    'sub_category_id'     => 999,
    'quantity'            => 1,
    'requirements'        => ['player_id' => '123456789', 'server' => 'Asia'],
    'expected_unit_price' => $price['price']['unit_price'],
]);

// 5. Read it back until it settles.
do {
    sleep(20);
    $latest = s2t('/orders/' . $orderId);
} while ($latest['order']['status'] === 'pending');

echo $latest['order']['status'];

Dos detalles vale la pena copiarlos tal cual: guarde el UUID del pedido antes de la llamada de creación, no después, y vuelva a leer el precio en lugar de confiar en un número cacheado. Esos dos hábitos eliminan casi todos los cobros duplicados y todos los rechazos PRICE_INCREASED.

Webhooks: avisos de estado de pedido

Registre un endpoint HTTPS y la plataforma le envía los resultados de los pedidos a medida que ocurren, así su tienda deja de consultar en bucle y pasa a reaccionar. El aviso viene firmado, de modo que puede comprobar que el contenido salió de nosotros antes de actuar sobre él.

Eventos

EventoSe dispara cuando
order.completedThe order finished and everything it owed the buyer was delivered.
order.failedThe order failed and no sub-transaction is left pending or processing.
order.refundedThe order was fully refunded back to your wallet.
webhook.testYou triggered a signed test delivery from the panel to check your handler.

Encabezados de entrega

EncabezadoValor
Content-Typeapplication/json
X-Shop2Topup-Signaturesha256=<hex> — HMAC-SHA256 of the raw request body
X-Shop2Topup-EventEvent name, e.g. order.completed
User-Agentshop2topup-webhook/1.0

Contenido del aviso

Todos los avisos tienen los mismos tres campos de primer nivel — event, timestamp y data — y data replica el objeto de pedido que devuelve el endpoint de estado. Los códigos de gift card solo aparecen en un pedido completed, player_name puede ser null, y sub_transaction_summary aparece siempre que el pedido se haya dividido en unidades.

Cuerpo del POST - order.completed
{
  "event": "order.completed",
  "timestamp": "2026-03-11T14:30:00.000Z",
  "data": {
    "order_id": "01912345-6789-7abc-8def-0123456789ab",
    "status": "completed",
    "player_id": "123456789",
    "player_name": "ProGamer99",
    "subcategory_name": "FF 100 Diamonds",
    "quantity": 1,
    "charged_amount": "0.950000",
    "currency": "USD",
    "created_at": "2026-03-11T14:28:00.000Z",
    "completed_at": "2026-03-11T14:30:00.000Z",
    "vouchers": [
      {
        "code": "XXXX-YYYY-ZZZZ",
        "serial_number": "SN-9928371",
        "expiry_date": "2027-03-11"
      }
    ],
    "sub_transaction_summary": {
      "total": 1,
      "completed": 1,
      "failed": 0,
      "pending": 0,
      "processing": 0,
      "retrying": 0,
      "refunded": 0
    }
  }
}

Verificación de la firma

Cada entrega lleva X-Shop2Topup-Signature, formado por sha256= seguido del HMAC-SHA256 hexadecimal del cuerpo crudo de la solicitud bajo su secreto de webhook. Verifíquelo contra los bytes crudos, antes de cualquier parseo de JSON, y compare en tiempo constante. Un handler que primero parsea y después verifica se puede engañar con un cuerpo re-serializado.

Node.js - verificar y confirmar
// Express — verify the signature against the RAW body, not the parsed object.
const crypto = require('crypto');

app.post(
  '/shop2topup/webhook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.get('X-Shop2Topup-Signature') || '';
    const expected =
      'sha256=' +
      crypto
        .createHmac('sha256', process.env.S2T_WEBHOOK_SECRET)
        .update(req.body)
        .digest('hex');

    const a = Buffer.from(expected);
    const b = Buffer.from(signature);
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.status(401).end();
    }

    const payload = JSON.parse(req.body.toString('utf8'));

    // Answer fast, then do the work. The delivery times out after 10 seconds.
    res.status(200).end();
    enqueueSettlement(payload.data.order_id, payload.event);
  },
);

Entrega y reintentos

El aviso se intenta una sola vez y su endpoint tiene diez segundos para responder. No hay reintento automático ni backoff exponencial detrás, y es deliberado: el endpoint de estado es el registro autoritativo, así que la conciliación es una consulta que usted controla y no un calendario de reintentos que no puede ver. Devuelva 200 de inmediato y haga el trabajo en su propia cola.

Handlers idempotentes

Indexe su handler por order_id y haga que reprocesar un aviso no tenga efecto. Incluso con un solo intento de entrega, su propia infraestructura alguna vez le pasará el mismo mensaje a dos workers, y un pedido que acredita dos veces a un cliente es un bug mucho peor que uno que acredita tarde.

Registro y secretos

Registrar la URL de aviso, enviar una entrega de prueba firmada y rotar el secreto de firma se hacen dentro de su panel de revendedor con sesión iniciada, no por la API pública. HTTPS es obligatorio y las direcciones privadas se rechazan.

Abrir API Access en el panel de revendedor

Límites de solicitudes y manejo de errores

Los límites son públicos, se aplican por cuenta y se calculan sobre una ventana deslizante. El trabajo de lectura del catálogo tiene el tope más alto; los endpoints de estado son ajustados a propósito, porque existen como respaldo de conciliación y no como bucle de consulta.

RutaSolicitudesVentanaSe cuentan por
GET /catalog/* (all five)8060sper account
POST /orders/create12060sper account
GET /orders/:orderId360sper account + order id
POST /orders/batch260sper account
GET /orders260sper account
GET /account6060sper account
POST /player/validatededicated limiterper account

Cada respuesta lleva X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset, así que un cliente bien educado nunca tiene que adivinar en qué punto está. Vigile el contador restante y baje el ritmo antes de que lo limiten, no después.

Respuesta limitada - 429
HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Limit: 80
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1765000000

{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded. Try again in 45 seconds.",
    "details": {
      "limit": 80,
      "remaining": 0,
      "retry_after": 45,
      "window_seconds": 60
    }
  }
}

Los errores que verá primero, y cómo resolverlos

Los fallos siempre devuelven un código estable junto al estado HTTP. Estos son los que encuentra una integración nueva en su primera semana; cada uno viene con el cambio que lo resuelve.

CódigoHTTPQué significaCómo resolverlo
INVALID_API_KEY401Invalid API keyThe provided API key does not match any active key in the system.Verify your key ID and secret are correct. Regenerate the key from the API Access panel if needed.
IP_NOT_ALLOWED403IP address not in allowlistThe request originated from an IP address not in your API key allowlist.Add your server IP to the allowlist in the API Access panel, or remove IP restrictions.
MISSING_REQUIRED_FIELD400Missing required fieldA required field is missing from the request body.Check the endpoint documentation for required fields.
INVALID_UUID_FORMAT400Invalid UUID formatThe order_id is not a valid UUID format.Use any valid UUID format (v1, v4, v7, etc). Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
PLAYER_NOT_FOUND400Player not foundThe player ID could not be found in the game system.Verify the player ID is correct. Check if a zone_id is required for the game.
REGION_MISMATCH400Region mismatchThe player belongs to a different region than expected.Use the correct zone_id or genshin_zone for the player region.
INSUFFICIENT_BALANCE400Insufficient wallet balanceYour wallet does not have enough funds to complete this order.Top up your wallet from the Reload page before placing the order.
OUT_OF_STOCK400Product is out of stockThe requested product is currently unavailable.Try again later or choose a different product.
DUPLICATE_ORDER409Duplicate order IDAn order with this UUID already exists. This is an idempotency protection.Generate a new UUID for a new order. If retrying, use the same UUID to get the existing order.
PRICE_INCREASED400Price has increased beyond expectedThe current unit price is higher than the expected_unit_price you provided.Fetch the latest price with GET /catalog/subcategory/:id/price and update expected_unit_price.
RATE_LIMIT_EXCEEDED429Rate limit exceededYou have exceeded the rate limit for this endpoint. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. When exceeded, the 429 response body contains: limit, remaining (0), retry_after (seconds to wait), and window_seconds. A Retry-After header is also set.Wait for the number of seconds indicated by retry_after or the Retry-After header before retrying. Monitor X-RateLimit-Remaining headers to avoid hitting limits.
SERVICE_UNAVAILABLE503Service temporarily unavailableThe service is temporarily down for maintenance or overloaded.Wait and retry after a few minutes.
Ver la referencia completa de códigos de error — los 32 códigos con su solución

Preguntas de integración, respondidas

Las preguntas que los desarrolladores hacen de verdad mientras conectan todo esto: autenticación, reintentos, idempotencia, límites, avisos y manejo de fallos.

¿Cómo autentica las solicitudes la API para revendedores?

Con un solo encabezado HTTP: Authorization: Bearer <keyId>.<secret>. El par de claves se emite desde la página API Access de su panel de revendedor, el secreto se muestra una única vez al crearlo, y el servidor lo verifica con una comprobación HMAC en cada llamada. No hay redirección OAuth, ni cookie de sesión, ni token que refrescar, así que el mismo encabezado sirve para la primera solicitud y para todas las siguientes.

¿A qué URL base y versión debe apuntar mi cliente?

A una sola URL base, y es la de producción: no hay un host de pruebas aparte contra el que desarrollar y que luego se cambie. Apunte el cliente a la URL base v1 que aparece arriba, envíe Authorization: Bearer <keyId>.<secret> en cada petición y empiece por GET /account para comprobar la clave y la forma de la cabecera antes que nada. POST /orders/create es la única llamada que mueve dinero: déjela para el final, ejecútela con su denominación más pequeña y envíe expected_unit_price mientras termina de ajustar el cliente.

¿Qué campo lleva la clave de idempotencia en POST /orders/create?

El campo order_id: un UUID que usted genera en su lado y envía en el cuerpo de la creación. Si repite una creación con un order_id que ya existe, la API responde HTTP 409 con DUPLICATE_ORDER en lugar de abrir un segundo pedido, así que un reintento nunca se convierte en un segundo cobro; lea el pedido original con GET /orders/:orderId. Genere y guarde el UUID antes del primer intento, porque un UUID creado después de un timeout es una clave nueva y no una clave de reintento, y el servidor no fusionará dos order_id que significaban lo mismo.

Mi solicitud expiró. ¿Se creó el pedido?

Averígüelo con una lectura de estado en lugar de adivinar. Llame a GET /orders/:orderId con el mismo UUID que envió: si el pedido aparece, la expiración ocurrió después de que fuera aceptado y no debe reenviarlo. Si obtiene ORDER_NOT_FOUND, no se cobró nada y es seguro repetir la solicitud de creación con ese mismo UUID.

¿Cómo verifico la firma de una llamada de webhook?

Recalcule el HMAC sobre el cuerpo crudo de la petición y compárelo con la cabecera X-Shop2Topup-Signature. La cabecera lleva sha256=<hex>: calcule HMAC-SHA256 de los bytes exactos que recibió con su secreto de webhook, añada el prefijo sha256= y compare en tiempo constante, nunca contra un objeto JSON reserializado, porque parsear y volver a serializar cambia los bytes y la comparación no coincidirá jamás. Rechace lo que no cuadre con un 401, responda 200 a una entrega válida en menos de diez segundos y haga la conciliación después en su propia cola.

¿Qué pasa cuando llego al límite de solicitudes?

Recibe un HTTP 429 con el código RATE_LIMIT_EXCEEDED y un encabezado Retry-After. El cuerpo de la respuesta repite esa misma cifra en retry_after y agrega limit, remaining y window_seconds. Espere exactamente esa cantidad de segundos y reintente una vez; no reintente de inmediato ni reparta la misma llamada entre varias claves, porque el contador es por cuenta y no por conexión.

¿Qué campos devuelve POST /player/validate?

player_name y, cuando el juego los expone, zone_id y region, dentro del objeto data de la respuesta. La creación del pedido resuelve al jugador y comprueba la región por su cuenta, y un intento fuera de región se rechaza con REGION_MISMATCH (HTTP 400) antes de cualquier cargo en el monedero, así que no es esta llamada la que protege su saldo. Llámela porque leer player_name es la única forma práctica de mostrar al comprador la cuenta que va a recargar y de detectar un identificador mal escrito antes de que confirme: el nombre siempre viene del juego, nunca de lo que el comprador escribió.

¿Cómo convierto la respuesta requirements en el cuerpo del pedido?

Indéxela por field_name: cada entrada que devuelve GET /catalog/category/:categoryId/requirements se convierte en una propiedad del objeto requirements que envía a POST /orders/create. Cada entrada trae además data_type, con lo que su formulario sabe qué renderizar y su servidor qué convertir, y las entradas single_select y multi_select incluyen sus valores permitidos: ofrezca esos en lugar de un campo de texto libre. Cachee la lista por categoría en vez de leerla en cada pedido, y vuelva a leerla cuando una creación responda MISSING_REQUIRED_FIELD.

¿Cómo debe tratar mi cliente una respuesta PRICE_INCREASED?

Relea el precio, decida y reenvíe; nunca repita el mismo cuerpo. PRICE_INCREASED (HTTP 400) significa que el precio unitario vigente está por encima del expected_unit_price que envió, así que la creación se rechazó y no se cobró nada; los detalles del error llevan expected_unit_price y current_unit_price, y un reintento ciego con la cifra antigua se rechazará siempre. Lea GET /catalog/subcategory/:itemId/price, decida si la cifra nueva le sigue sirviendo y reenvíe la creación con el expected_unit_price actualizado y el mismo order_id: un intento rechazado no lo consumió. Omitir expected_unit_price desactiva del todo la protección, que casi nunca es lo que quiere una integración automatizada.

¿Cómo aparece en la respuesta un pedido entregado en parte?

Como status partial, con el desglose en sub_transaction_summary. Ese objeto cuenta las unidades por estado —completed, refunded, failed, pending, processing, retrying— y sub_transactions trae la fila que hay detrás de cada contador, de modo que su lógica de conciliación lee números en lugar de deducirlos de una sola palabra. Trate pending y processing como no terminales y siga esperando; trate completed, partial, failed y refunded como terminales. Las llamadas order.completed, order.failed y order.refunded llevan el mismo resumen, así que un payload con la firma verificada basta para cerrar sin una segunda lectura.

¿Qué recibe una petición desde una dirección fuera de la lista permitida?

Un HTTP 403 con el código IP_NOT_ALLOWED, devuelto antes de ejecutar ninguna lógica de negocio. La comprobación se aplica siempre que la lista de IP de la clave no esté vacía, lo que deja inservible una clave filtrada fuera de sus propias direcciones de salida. Ramifique según error.code y no según el texto del mensaje, y lea un IP_NOT_ALLOWED repentino en producción como un cambio de dirección de salida —una pasarela NAT nueva o una subred de workers nueva— y no como una clave rota. Deje la lista vacía solo si su dirección saliente realmente no es estable.

¿Cuánto tarda en completarse un pedido?

Trate la finalización como asíncrona, nunca como instantánea. POST /orders/create responde apenas el pedido se acepta y la billetera se debita, normalmente con estado pending; la entrega corre después en workers separados y el estado final le llega por webhook o en su siguiente lectura de estado. No bloquee una solicitud HTTP de cara al cliente esperando el estado final: acéptela, respóndale a su cliente y resuelva el resto por fuera.

Cree una cuenta, genere una clave y salga a producción

No hay fila de aprobación ni período de espera entre registrarse y llamar a la API. Cree la cuenta de revendedor, genere una clave en la página API Access, cargue saldo, y su primer pedido puede salir el mismo día.

¿No es usted quien programa este proyecto? El lado comercial está cubierto en la página del programa de revendedores de SHOP2TOPUP.

API de recargas de juegos para revendedores — Documentación