Desarrolladores/Integración

Catálogo y pedidos

Esta guía recorre todo el proceso de compra: consultar tu saldo, buscar un producto, ver su precio, hacer el pedido y leer los códigos.

Ver también: Autenticación · Convenciones · Webhooks

#Cuenta y saldo

#Cuenta

GET/api/v1/account muestra los datos de tu empresa y si la API está activada.

JSON
{
  "merchant_id": "M00000001",
  "name": "Acme Shop",
  "legal_name": "Acme Shop Ltd",
  "billing_email": "[email protected]",
  "status": "active",
  "kyb_status": "approved",
  "api_access_enabled": true,
  "default_currency": "USD",
  "purchase_limits": {
    "currency": "USD",
    "verified": true,
    "max_order_amount": "1000.00",
    "daily_order_amount_limit": "10000.00",
    "daily_order_count_limit": 1000,
    "used_today_amount": "120.50",
    "used_today_count": 3,
    "day_resets_at": "2026-10-01T00:00:00+00:00"
  }
}
  • default_currency es la moneda de tu billetera. Todo lo que pagas está en esa moneda.
  • api_access_enabled pasa a true cuando CardV aprueba tu empresa.

#Saldo

GET/api/v1/balance muestra cuánto dinero puedes gastar.

JSON
{
  "currency": "USD",
  "balance": "1520.40",
  "reserved_amount": "0.00",
  "available_balance": "1520.40",
  "low_balance_threshold": "200.00",
  "low_balance_notified_at": null,
  "is_active": true
}
  • available_balance es lo que puedes gastar ahora mismo. Es balance menos reserved_amount.
  • Un pedido mayor que available_balance se rechaza y no se cobra nada.
  • low_balance_threshold es el monto a partir del cual recibes el correo de saldo bajo. Lo configuras en el Portal.
  • Para agregar fondos, usa el Portal.

#Tipo de cambio

GET/api/v1/fx-rates?quote=EUR devuelve el tipo de cambio de USD a otra moneda.

Úselo si su tienda vende en una moneda distinta de USD. CardV siempre cobra su monedero en USD. El tipo de cambio solo le ayuda a fijar sus propios precios.

JSON
{
  "base": "USD",
  "quote": "EUR",
  "rate": "0.9234",
  "as_of": "2026-10-11T07:05:00Z"
}
  • quote es un código de moneda ISO 4217, como EUR, GBP o JPY.
  • base es opcional. Solo se admite USD.
  • rate indica cuántas unidades de quote obtiene por 1 USD. Es una cadena decimal. No la lea como float.
  • as_of es cuándo CardV actualizó este tipo de cambio por última vez. Los tipos se actualizan cada hora.
  • quote=USD devuelve "rate": "1".
  • Guarde el tipo de cambio en su sistema. Basta con consultarlo unas pocas veces al día.
EstadoSignificadoQué hacer
400Falta quote o no es un código de 3 letras, o base no es USDCorrija la solicitud
404No exchange rate for this currency.Fije usted los precios en esta moneda
503Exchange rate temporarily unavailable.Siga usando el último tipo guardado y vuelva a intentarlo más tarde

Firme esta solicitud como cualquier otra. La ruta firmada incluye la cadena de consulta, por ejemplo /api/v1/fx-rates?quote=EUR.

#Productos (SKUs)

Un SKU es un producto concreto que puedes comprar, por ejemplo "Steam Wallet 10 USD". Su ID tiene este formato: S000456. Lo que cotizas y pides son SKUs.

#Modelos de producto

Cada SKU corresponde a uno de estos modelos. Lee denomination_type y product_type para distinguirlos.

ModeloCómo reconocerloPrecioPedidoQué recibes
Monto fijodenomination_type: "fixed"merchant_price es el precio de una unidad.Envía quantity.Códigos en deliveries.
Monto variabledenomination_type: "range"De merchant_price_min a merchant_price_max. El precio real depende del monto: solicita una cotización.Envía quantity y amount.Códigos en deliveries.
Recarga directaproduct_type: "direct_charge"Igual que el monto fijo.Envía también inputs (ver recargas directas).Normalmente no hay códigos. succeeded significa que la cuenta se recargó.
Recarga de celularNo está en este catálogoSu propia cotizaciónSu propia APIVer Recargas de celular.
  • vertical es la línea de producto, no el modelo. Una recarga directa puede tener vertical: "gift_card".
  • Cualquier SKU de monto fijo o variable puede entregarse como un enlace de canje de CardV en lugar de un código. En ese caso el SKU tiene delivery_mode: "voucher". Recibes un enlace (delivery_type: "link", el enlace está en redeem_url). Tu cliente lo abre para obtener el código. Envíaselo a tu cliente igual que un código.

GET/api/v1/skus muestra los SKUs que puedes comprar. Ejemplo:

HTTP
GET /api/v1/skus?search=steam&region=US&limit=50&offset=0

Filtros (todos opcionales):

FiltroEjemploBusca por
searchsteamID de SKU, nombre o marca
brandSteamNombre de la marca (sin importar mayúsculas)
regionUSCódigo o nombre del país. GLC = global, EU = Europa
categoryTravelCategoría comercial, por ejemplo Travel, Digital Wallets & Payment, Game Credits
verticalgift_cardLínea de producto
product_typepin_codeForma de entrega

Paginación: envía limit (100 por defecto, máximo 500) y offset. Sigue pidiendo con un offset mayor hasta que offset llegue a count.

JSON
{
  "count": 7,
  "limit": 2,
  "results": [
    {
      "sku_id": "S000456",
      "product_id": "P000123",
      "name": "Steam Wallet 10 USD",
      "product_name": "Steam Wallet US",
      "brand": "Steam",
      "region": "US",
      "vertical": "gift_card",
      "product_type": "pin_code",
      "denomination_type": "fixed",
      "denomination_value": "10",
      "face_currency": "USD",
      "merchant_price": "9.25",
      "merchant_price_min": "9.25",
      "merchant_price_max": "9.25",
      "settlement_currency": "USD",
      "availability": "available",
      "min_quantity": 1,
      "max_quantity": 100,
      "required_input_schema": [],
      "...": "more fields"
    },
    {
      "sku_id": "S006126",
      "product_id": "P002210",
      "name": "Apple ES",
      "product_name": "Apple ES",
      "brand": "Apple",
      "region": "ES",
      "vertical": "gift_card",
      "product_type": "pin_code",
      "denomination_type": "range",
      "denomination_value": "0",
      "min_face_value": "2",
      "max_face_value": "250",
      "amount_step": null,
      "face_currency": "EUR",
      "merchant_price": "2.28",
      "merchant_price_min": "2.28",
      "merchant_price_max": "284.15",
      "price_label": "From 2.28 USD",
      "settlement_currency": "USD",
      "availability": "available",
      "...": "more fields"
    }
  ],
  "filter_options": {"brands": [], "regions": [], "categories": [], "verticals": []}
}

GET/api/v1/skus/{sku_id} devuelve un solo SKU con los mismos campos.

Los campos más útiles:

CampoQué significa
sku_idEl ID que usas para cotizar y pedir.
merchant_priceTu precio por unidad, en settlement_currency. En un SKU range es solo el precio de min_face_value, un precio inicial. No lo uses para otros montos: pide una cotización con amount.
availabilityavailable o unavailable. Pide solo SKUs available.
denomination_typefixed o range. Ver montos fijos y variables.
denomination_valueValor nominal de un SKU fixed. "0" en un SKU range.
min_face_value, max_face_valueSKU range: el monto mínimo y máximo que puede elegir tu cliente, en face_currency.
merchant_price_min, merchant_price_maxPrecio del monto mínimo y del máximo. En SKU fixed son iguales a merchant_price. Úsalos para mostrar un rango de precios; antes de pedir, solicita siempre una cotización del monto exacto. merchant_price_max es "" cuando ahora mismo no se puede calcular su precio.
price_labelmerchant_price como texto para mostrar. En SKU range empieza por From, por ejemplo From 2.28 USD.
face_value_labelValor nominal como texto para mostrar, por ejemplo 10 USD o 50 - 1000 GBP.
discountDescuento sobre el valor nominal, por ejemplo 7.50% off face value. Solo en SKU fixed cuya moneda de la tarjeta es la de tu billetera; en otro caso, "".
amount_stepSolo SKUs de monto variable. El monto debe ser múltiplo de este valor; por ejemplo, "1" significa solo números enteros. null significa que no hay paso.
face_currencyMoneda impresa en la tarjeta. Puede ser distinta a la de tu billetera.
min_quantity, max_quantityCuántas unidades puede tener una línea del pedido.
product_typepin_code (recibes un código) o direct_charge (recargamos una cuenta).
delivery_typeLo que entrega el proveedor: code, code_pin, code_serial, url o direct_charge. Si delivery_mode es voucher, recibes un enlace en su lugar.
delivery_modecode (recibes el propio código) o voucher (recibes un enlace de canje de CardV).
voucher_instructionsEn SKU voucher: texto que explica a tu cliente cómo usar el enlace. En otro caso, "".
vertical, categoryLínea de producto y categoría de negocio, para filtrar y mostrar.
region, region_name, region_labelCódigo de país o zona, su nombre y una etiqueta para mostrar.
required_input_schemaDatos que debes enviar para las recargas directas.
brand_logo_url, image_urlImágenes alojadas por CardV, o "".
description, redemption_instructions, terms, disclaimerTextos que puedes mostrar a tus clientes.

Consejos:

  • Solo ves los SKUs activos y habilitados para tu cuenta. Los demás devuelven 404.
  • Sincroniza la lista de SKUs cada 5 a 15 minutos. Pide siempre una cotización justo antes de hacer el pedido.
  • Muestra a tus clientes los precios de tu lista sincronizada. No llames al endpoint de cotización en cada visita a la página ni por cada visitante.
  • Los precios de los SKUs de monto variable siguen el costo del proveedor y pueden cambiar varias veces al día. Actualiza merchant_price_min y merchant_price_max al sincronizar tus SKUs.
  • filter_options muestra las marcas, regiones y líneas de producto por las que puedes filtrar.

#Sincronizar por producto

Si tu tienda muestra una página por producto con selector de monto, sincroniza productos en lugar de SKU. GET/api/v1/products lista productos. Cada producto incluye todos los SKU que puedes comprar en él.

HTTP
GET /api/v1/products?brand=Steam&region=GB&limit=50&offset=0

Admite los mismos filtros y la misma paginación que GET/api/v1/skus. count cuenta productos y sku_count cuenta sus SKU.

JSON
{
  "count": 1,
  "sku_count": 4,
  "limit": 50,
  "results": [
    {
      "product_id": "P000123",
      "product_name": "Steam Wallet UK",
      "brand": "Steam",
      "category": "Gaming",
      "region": "GB",
      "product_type": "pin_code",
      "image_url": "https://b2b.cardv.net/api/v1/catalog-assets/steam.png",
      "required_input_schema": [],
      "skus": [
        {
          "sku_id": "S000456",
          "name": "Steam Wallet 5 GBP",
          "label": "5 GBP",
          "denomination_type": "fixed",
          "denomination_value": "5",
          "face_currency": "GBP",
          "merchant_price": "7.61",
          "settlement_currency": "USD",
          "availability": "available",
          "min_quantity": 1,
          "max_quantity": 100,
          "...": "more fields"
        }
      ]
    }
  ],
  "filter_options": {"brands": [], "regions": [], "categories": [], "verticals": []}
}

GET/api/v1/products/{product_id} devuelve un producto con sus SKU, además de description, redemption_instructions, terms y disclaimer.

  • Cada SKU de skus tiene los mismos campos y valores que en GET/api/v1/skus. Cotiza y pide con su sku_id como siempre.
  • Usa label como texto de la opción, por ejemplo 5 GBP o Tinder Plus 1 Month. Algunos productos son planes, no montos.
  • Un producto tiene una sola moneda de valor nominal y un solo tipo de monto. Un SKU de monto variable siempre es un producto aparte.
  • La lista no incluye los textos largos. Léelos una vez por producto en GET/api/v1/products/{product_id}.

#Montos fijos y variables

La mayoría de los SKUs tienen un valor nominal fijo, como 10 USD. Algunos tienen un rango: tu cliente elige el monto, por ejemplo de 5 a 500 USD.

TipoAl cotizarAl hacer el pedido
fixedEnvía quantityNo envíes amount
rangeEnvía quantity y amountEnvía amount

En un SKU de monto variable, amount debe estar entre min_face_value y max_face_value. Se expresa en face_currency.

Precio de un SKU de monto variable. En las listas de SKU y de productos, merchant_price y price_label son solo el precio de min_face_value. merchant_price_min y merchant_price_max dan el precio del monto mínimo y del máximo. El precio de un monto intermedio no es un múltiplo simple de ninguno de ellos. Antes de fijar tu precio de venta o de pedir, solicita una cotización con el amount exacto y envía su merchant_price como expected_unit_price.

Algunos SKUs de monto variable también tienen un amount_step. Si no es null, amount debe ser múltiplo de él. Por ejemplo, con "amount_step": "1" puedes enviar 25 pero no 25.50. Una cotización o un pedido con un monto que no encaja en el paso devuelve HTTP 400 y no se cobra nada.

#Recargas directas

Algunos productos recargan directamente la cuenta de tu cliente, por ejemplo una cuenta de juego. Para estos, CardV necesita los datos de la cuenta. El SKU indica cuáles son:

JSON
"required_input_schema": [
  {"key": "player_id", "label": "Player ID", "type": "text", "required": true},
  {"key": "server", "label": "Server", "type": "text", "required": false}
]

Envía los valores en el campo inputs de la línea del pedido, usando cada key:

JSON
"inputs": {"player_id": "123456789", "server": "EU"}
  • Todos los campos son obligatorios, salvo los que indican "required": false. type es una pista para el campo de tu formulario, por ejemplo text.
  • Si falta un valor obligatorio, el pedido se rechaza con un error en items.
  • Estos valores son datos personales de tu cliente. Protégelos (ver Seguridad).

#Cotización

Una cotización te dice el precio actual para una cantidad.

HTTP
GET /api/v1/skus/S000456/quote?quantity=2
GET /api/v1/skus/S000789/quote?quantity=1&amount=25.00
JSON
{
  "sku_id": "S000456",
  "settlement_currency": "USD",
  "merchant_price": "9.25",
  "quantity": 2,
  "total_price": "18.50",
  "min_quantity": 1,
  "max_quantity": 100,
  "availability": "available"
}
  • Una cotización no reserva el precio. Los precios pueden cambiar en cualquier momento.
  • Cotiza una sola vez, cuando tu cliente pase a pagar, con un tiempo de espera corto, por ejemplo 5 segundos. Si la cotización agota el tiempo, sigue mostrando tu precio sincronizado y vuelve a intentarlo; nunca muestres una página de error porque una cotización tardó.
  • Para protegerte, envía merchant_price como expected_unit_price al hacer el pedido. Envía el precio de una cotización, no uno que hayas calculado tú: debe coincidir exactamente, al céntimo. Si el precio cambió, CardV rechaza el pedido y no te cobra nada.
  • Una cantidad o un monto fuera de rango, o un monto que no es múltiplo de amount_step, devuelve HTTP 400 con un error en quantity o amount.

#Hacer un pedido

POST/api/v1/orders compra uno o más SKUs y los paga con tu billetera. Como todas las llamadas, tiene que ir firmada.

JSON
{
  "external_order_id": "SHOP-20260929-10001",
  "items": [
    {"sku_id": "S000456", "quantity": 2, "expected_unit_price": "9.25"},
    {"sku_id": "S000789", "amount": "25.00", "expected_unit_price": "23.75"},
    {
      "sku_id": "S000900",
      "expected_unit_price": "4.90",
      "inputs": {"player_id": "123456789"}
    }
  ]
}
CampoObligatorioQué significa
external_order_idSíTu número de pedido, de 1 a 120 caracteres. No se puede repetir.
itemsSíUna o más líneas del pedido.
items[].sku_idSíEl SKU que quieres comprar.
items[].quantityNoCuántas unidades. 1 por defecto.
items[].amountSKUs de monto variableEl valor nominal que quieres comprar. Debe respetar min_face_value, max_face_value y amount_step.
items[].expected_unit_priceRecomendadoEl merchant_price de la cotización. Envíalo siempre.
items[].inputsRecargas directasDatos de la cuenta para las recargas directas.

Cuando CardV acepta el pedido, cobra el total completo de tu billetera en ese momento. Después, la entrega se procesa en segundo plano.

La respuesta es HTTP 201:

JSON
{
  "idempotent_replay": false,
  "order": {
    "order_id": "O-00001234",
    "external_order_id": "SHOP-20260929-10001",
    "status": "accepted",
    "total_amount": "46.20",
    "...": "more fields"
  }
}

Guarda order.order_id. Esta respuesta nunca incluye los códigos. Los lees después (Consultar un pedido).

#Pedidos rechazados

Un pedido rechazado devuelve HTTP 400 y no se cobra nada. La clave del error te dice por qué:

ClaveCausaQué hacer
itemsCambió el precio, el SKU no está disponible, el monto no es válido o falta un datoCotiza de nuevo, corrige y vuelve a enviar
balanceNo hay suficiente dinero en tu billeteraAgrega fondos en el Portal
riskSuperaste el tamaño máximo de pedido o tu límite diarioContacta a CardV
external_order_idTu número de pedido ya se usó en otro pedido distintoVer Reintentar sin riesgo

Ejemplo de un cambio de precio:

JSON
{
  "items": "SKU S000456 price changed from 9.25 to 9.41 USD; refresh quote and confirm again."
}

Tu cuenta tiene límites por pedido, de gasto diario y de cantidad de pedidos al día. GET/api/v1/account los muestra en purchase_limits, junto con lo que ya usaste hoy. 0 significa sin límite. Los límites diarios se reinician a las 00:00 UTC.

#Reintentar sin riesgo

Tu número de pedido (external_order_id) evita que compres dos veces. Si vuelves a enviar el mismo pedido con el mismo número de pedido, CardV no te cobra otra vez. Te devuelve el pedido que ya existe.

Si envíasRecibes
Un número de pedido nuevoHTTP 201. Un pedido nuevo. Se cobra de tu billetera.
El mismo número de pedido y el mismo pedidoHTTP 200 y "idempotent_replay": true. El pedido que ya existía. Sin cobro.
El mismo número de pedido pero otro pedidoHTTP 400 en external_order_id. No pasa nada.

"El mismo pedido" significa las mismas líneas, en el mismo orden, con el mismo SKU, cantidad, monto e inputs. Si envías expected_unit_price, debe coincidir con el precio del primer pedido.

CardV reconoce un pedido repetido antes de revisar el saldo y el precio. Por eso siempre devuelve el primer pedido, aunque el precio haya cambiado desde entonces.

#Flujo de reintento seguro

Si no recibes una respuesta clara, simplemente vuelve a enviar el mismo pedido.

Text
POST /orders con el número de pedido R
 ├─ 201 o 200 → guarda order_id. Listo.
 ├─ 400 items / balance / risk → no se creó ningún pedido.
 │      Corrige la causa y vuelve a enviar. Puedes reutilizar R.
 ├─ 400 external_order_id → R pertenece a otro pedido. Detente y revisa.
 ├─ 403 error de firma → firma de nuevo y envía el mismo cuerpo.
 ├─ 429 → espera lo que indica Retry-After, firma de nuevo y envía el mismo cuerpo.
 └─ tiempo agotado, 5xx o conexión perdida
        → vuelve a enviar el mismo cuerpo con el mismo R.
          Recibes 201 (el primer intento no llegó) o 200 (sí llegó).

Reglas:

  • Nunca crees un número de pedido nuevo porque se perdió una respuesta. Si la primera solicitud sí llegó, un número nuevo compraría todo dos veces.
  • Cada reenvío necesita una marca de tiempo, un nonce y una firma nuevos. El cuerpo no cambia.

#Consultar un pedido

GET/api/v1/orders/{order_id} devuelve el pedido, su estado y sus códigos.

JSON
{
  "order_id": "O-00001234",
  "external_order_id": "SHOP-20260929-10001",
  "status": "succeeded",
  "currency": "USD",
  "total_amount": "18.50",
  "created_at": "2026-09-29T08:15:30.123456Z",
  "updated_at": "2026-09-29T08:15:41.004211Z",
  "items": [
    {
      "sku_id": "S000456",
      "product_name": "Steam Wallet US",
      "quantity": 2,
      "unit_price": "9.25",
      "total_price": "18.50",
      "delivery_count": 2,
      "deliveries": [{"...": "see Codes below"}]
    }
  ],
  "...": "more fields"
}
  • total_amount es lo que se te cobró cuando se aceptó el pedido.
  • items[].unit_price es el precio fijado para este pedido.
  • items[].deliveries contiene los códigos completos. Trata esta respuesta como información secreta.
  • invoice_url y delivery_file_url son rutas a la factura y a los códigos en CSV. Las dos solo funcionan en el Portal. Con una clave de API devuelven HTTP 403.
  • También vienen id (un número antiguo, no lo uses), events (un historial, solo para mostrar) y el avance de entrega de cada línea. Puedes ignorarlos.
  • Un ID de pedido desconocido devuelve HTTP 404.

#Estado del pedido y códigos

#Estado

Text
accepted ──► processing ──► succeeded
                  │
                  ├──► partially_succeeded   (algunas líneas se entregaron y otras no)
                  │
                  └──► failed ──► refunded   (el dinero vuelve a tu billetera)
Estado¿Terminado?Qué hacer
acceptedNoEspera. Ya se cobró de la billetera, pero la entrega no ha empezado.
processingNoEspera. No vuelvas a hacer el pedido.
succeededSíLee los códigos y entrégaselos a tu cliente.
partially_succeededSíEntrega lo que llegó. El resto se reembolsa más adelante.
failedTodavía noEspera a que pase a refunded. Fallido todavía no significa reembolsado.
refundedSíEl dinero ya volvió a tu billetera.

Si no usas webhooks, consulta el pedido así: a los 5 segundos, luego a los 10 s, 30 s y 60 s, y después cada 5 minutos. Respeta el límite de solicitudes. La mayoría de los pedidos terminan en segundos. Algunos necesitan una revisión manual y pueden tardar horas.

#Códigos

Cada unidad entregada es un objeto dentro de items[].deliveries:

JSON
{
  "status": "stored",
  "delivery_type": "card_pin",
  "card_number": "X1234",
  "pin_code": "9876",
  "redeem_url": "",
  "expiry_date": "2027-09-29",
  "instructions": "Redeem at ..."
}
CampoQué es
card_numberEl código principal: número de tarjeta, código de cupón o PIN.
pin_codeEl segundo código, como un PIN o código de seguridad. Vacío cuando no hay.
redeem_urlEnlace de canje. En las entregas link, el propio enlace es el código.
expiry_dateÚltimo día en que se puede usar el código, en formato YYYY-MM-DD. "" significa que el proveedor no indicó fecha de caducidad. No significa que el código haya caducado.
  • Entrega a tu cliente todos los campos que no estén vacíos: card_number, pin_code, redeem_url, expiry_date e instructions.
  • delivery_type indica qué recibiste: code, card_pin, link, code_link o qr.
  • Mantén redeem_url en secreto, igual que un código.
  • Nunca borres, ocultes ni anules un código porque expiry_date esté vacío. Muestra una fecha de caducidad a tu cliente solo cuando no esté vacía.
  • Nunca entregues a tu cliente una unidad cuyo status sea voided.
  • Los productos de recarga directa normalmente no tienen entregas. succeeded significa que la cuenta ya se recargó.
  • Los webhooks nunca incluyen códigos. Consulta el pedido cuando llegue un webhook.

¿Dudas sobre tu integración? Escribe a [email protected] con tu Merchant ID y el ID del pedido o de la solicitud.