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.
curl -s "https://shop2topup.com/api/endpoints/v1/catalog/categories?bigCategoryId=1" \
-H "Authorization: Bearer YOUR_KEY_ID.YOUR_KEY_SECRET"{
"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.
{
"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étodo | Ruta | Para qué sirve | Referencia |
|---|---|---|---|
| GET | /account | Get Account Info | Leer la referencia |
| GET | /catalog/big-categories | List Big Categories | Leer la referencia |
| GET | /catalog/categories | List Categories | Leer la referencia |
| GET | /catalog/subcategories | List Subcategories (Products) | Leer la referencia |
| GET | /catalog/subcategory/:itemId/price | Get Item Price | Leer la referencia |
| GET | /catalog/category/:categoryId/requirements | Get Category Requirements | Leer la referencia |
| POST | /player/validate | Validate Player | Leer la referencia |
| POST | /orders/create | Create Order | Leer la referencia |
| GET | /orders/:orderId | Get Order Status | Leer la referencia |
| POST | /orders/batch | Batch Get Orders | Leer la referencia |
| GET | /orders | List Orders | Leer 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.
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 cuentaUna 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
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
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
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
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ódigoOcultar 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ódigoOcultar 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ódigoOcultar 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
| Evento | Se dispara cuando |
|---|---|
| order.completed | The order finished and everything it owed the buyer was delivered. |
| order.failed | The order failed and no sub-transaction is left pending or processing. |
| order.refunded | The order was fully refunded back to your wallet. |
| webhook.test | You triggered a signed test delivery from the panel to check your handler. |
Encabezados de entrega
| Encabezado | Valor |
|---|---|
| Content-Type | application/json |
| X-Shop2Topup-Signature | sha256=<hex> — HMAC-SHA256 of the raw request body |
| X-Shop2Topup-Event | Event name, e.g. order.completed |
| User-Agent | shop2topup-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.
{
"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.
// 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 revendedorLí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.
| Ruta | Solicitudes | Ventana | Se cuentan por |
|---|---|---|---|
| GET /catalog/* (all five) | 80 | 60s | per account |
| POST /orders/create | 120 | 60s | per account |
| GET /orders/:orderId | 3 | 60s | per account + order id |
| POST /orders/batch | 2 | 60s | per account |
| GET /orders | 2 | 60s | per account |
| GET /account | 60 | 60s | per account |
| POST /player/validate | dedicated limiter | — | per 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.
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ódigo | HTTP | Qué significa | Cómo resolverlo |
|---|---|---|---|
| INVALID_API_KEY | 401 | Invalid 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_ALLOWED | 403 | IP 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_FIELD | 400 | Missing required fieldA required field is missing from the request body. | Check the endpoint documentation for required fields. |
| INVALID_UUID_FORMAT | 400 | Invalid 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_FOUND | 400 | Player 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_MISMATCH | 400 | Region mismatchThe player belongs to a different region than expected. | Use the correct zone_id or genshin_zone for the player region. |
| INSUFFICIENT_BALANCE | 400 | Insufficient 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_STOCK | 400 | Product is out of stockThe requested product is currently unavailable. | Try again later or choose a different product. |
| DUPLICATE_ORDER | 409 | Duplicate 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_INCREASED | 400 | Price 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_EXCEEDED | 429 | Rate 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_UNAVAILABLE | 503 | Service temporarily unavailableThe service is temporarily down for maintenance or overloaded. | Wait and retry after a few minutes. |
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.