QvaPay API
Marketplace

Productos

Crear, listar, editar y eliminar productos o servicios de una tienda, con control de inventario.

Gestión del catálogo de una tienda. Soporta tres tipos (kind): physical (físico, con envío), digital (entrega un payload al comprar) y service. El inventario es opcional por producto.


POST /market/products

Crea un producto en una tienda propia. La tienda puede estar pending; el catálogo se puede preparar antes de la aprobación.

Autenticación

Bearer Token. La tienda (shop_uuid) debe pertenecer al usuario.

Request

curl -X POST https://api.qvapay.com/market/products \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "shop_uuid": "a1b2c3d4-...",
    "title": "Camiseta de algodón",
    "description": "100% algodón orgánico",
    "kind": "physical",
    "price": 19.99,
    "stock": 50,
    "track_inventory": true,
    "main_image": "https://media.qvapay.com/market/<uuid>/product-1.webp"
  }'

Parámetros (body)

ParámetroTipoRequeridoDescripción
shop_uuidstringSíUUID de la tienda propia
titlestring (3-300)SíTítulo
descriptionstring (≤5000)NoDescripción
kindphysical | digital | serviceNo (default physical)Tipo
pricenumber (>0, ≤100000)SíPrecio en USD
currencystringNo (default USD)Moneda
stocknumber (≥0) | nullNoInventario. null = ilimitado
track_inventorybooleanNo (default true)Si es false, el stock pasa a ilimitado
digital_payloadstring (≤20000)NoEntregable (enlace/clave) — solo kind: digital
main_imagestring (URL)NoImagen principal
imagesstring[] (≤12)NoImágenes adicionales
categorynumberNoCategoría interna
skustring (≤64)NoCódigo interno del vendedor. Letras, números y . _ -. Único por tienda
ship_toobject | nullNoDestinos de envío (solo aplica a kind: physical). null u omitido = envía a cualquier destino. Ver formato abajo
activebooleanNo (default true)Visible

Formato de ship_to — objeto cuyas claves son países ISO-2 y cuyos valores son null (todo el país) o un array de códigos de provincia/estado:

{ "US": null, "CU": ["LHA", "MTZ"] }

Solo los países con datos de subdivisiones (por ahora US — estados USPS — y CU — 16 provincias con códigos de 3 letras: PRI, ART, LHA, MAY, MTZ, CFG, VCL, SSP, CAV, CMG, LTU, HOL, GRA, SCU, GTM, IJV) aceptan restricción por provincia; el resto solo admite null (todo el país). El checkout rechaza compras cuya dirección de envío quede fuera de estos destinos.

Response 201

Proyección del dueño (incluye digital_payload):

{
  "product": {
    "uuid": "p1q2r3...",
    "slug": "camiseta-de-algodon",
    "title": "Camiseta de algodón",
    "kind": "physical",
    "price": 19.99,
    "currency": "USD",
    "stock": 50,
    "track_inventory": true,
    "active": true,
    "featured": false,
    "sales_count": 0,
    "created_at": "2026-06-08T12:00:00.000Z"
  }
}

Errores

CódigoDescripción
401No autenticado
404Tienda no encontrada o no es del usuario
400Datos inválidos
409SKU ya usado en otro producto de la tienda

GET /market/products/{uuid}/variants

Ejes de opciones y variantes de un producto propio (incluye inactivas).

Autenticación

Bearer Token. Solo el dueño (vía la tienda).

Response

{
  "option_axes": ["Color", "Talla"],
  "variants": [
    { "uuid": "v1a2...", "sku": "CAM-R-M", "options": { "Color": "Rojo", "Talla": "M" }, "price": 21.99, "price_override": 21.99, "stock": 10, "image": null, "sort": 0, "active": true },
    { "uuid": "v3b4...", "sku": "CAM-R-L", "options": { "Color": "Rojo", "Talla": "L" }, "price": 19.99, "price_override": null, "stock": null, "image": null, "sort": 1, "active": true }
  ]
}

price es siempre el precio efectivo (el de la variante, o el del producto si no define uno); price_override distingue si la variante tiene precio propio (null = hereda).


PUT /market/products/{uuid}/variants

Sincroniza el set completo de variantes con el estado enviado: las filas con uuid se actualizan, las filas sin uuid se crean, y las variantes existentes que no aparezcan en el payload se eliminan. Idempotente — pensado para guardar la matriz completa del editor de una vez.

{ "option_axes": [], "variants": [] } elimina todas las variantes del producto.

Autenticación

Bearer Token. Solo el dueño.

Request

curl -X PUT https://api.qvapay.com/market/products/p1q2r3.../variants \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "option_axes": ["Color", "Talla"],
    "variants": [
      { "options": { "Color": "Rojo", "Talla": "M" }, "sku": "CAM-R-M", "price": 21.99, "stock": 10 },
      { "options": { "Color": "Rojo", "Talla": "L" }, "sku": "CAM-R-L", "price": null, "stock": null }
    ]
  }'

Parámetros (body)

ParámetroTipoRequeridoDescripción
option_axesstring[] (≤3)SíEjes de opciones (ej. ["Color","Talla"]), sin duplicados
variantsobject[] (≤100)SíEstado completo deseado. Cada fila: options (req — un valor por cada eje, combinación única), uuid (para actualizar una existente), sku (≤64, único por producto), price (null = hereda el precio base), stock (null = ilimitado), image (URL), active (default true), sort

El stock de la variante se descuenta atómicamente en cada compra (track_inventory del producto debe estar activo) y se repone en reembolsos.

Errores

CódigoDescripción
400Datos inválidos (ejes/combinaciones duplicados, opciones que no cubren los ejes, SKU repetido en el payload)
404Producto no encontrado o no es del usuario
409SKU duplicado en este producto

POST /market/products/bulk

Alta masiva de productos en una tienda propia, pensada para cargar un catálogo completo de una vez (importación desde CSV/Excel). Cada fila se valida y se crea de forma independiente: un error en una fila no detiene el resto. La tienda puede estar pending.

Autenticación

Bearer Token. La tienda (shop_uuid) debe pertenecer al usuario. Rate limit más estricto que el alta unitaria (token bucket: 1 cada 60s, ráfaga de 2).

Request

curl -X POST https://api.qvapay.com/market/products/bulk \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "shop_uuid": "a1b2c3d4-...",
    "products": [
      { "title": "Camiseta de algodón", "kind": "physical", "price": 19.99, "stock": 50, "image_url": "https://ejemplo.com/camiseta.jpg" },
      { "title": "Curso de diseño", "kind": "digital", "price": 9.99, "digital_payload": "https://drive.example.com/curso" }
    ]
  }'

Parámetros (body)

ParámetroTipoRequeridoDescripción
shop_uuidstringSíUUID de la tienda propia
productsobject[] (1-100)SíFilas de producto. Máximo 100 por request

Cada fila de products acepta los mismos campos que POST /market/products (sin shop_uuid), con coerción tolerante de tipos (números y booleanos pueden venir como texto, p. ej. "true", "50"). En lugar de main_image, usa:

ParámetroTipoRequeridoDescripción
image_urlstring (URL https)NoEl servidor descarga la imagen, la reprocesa (máx 5MB, JPG/PNG/WEBP) y la sirve desde media.qvapay.com. Si la descarga falla, el producto se crea sin imagen y se devuelve un warning en esa fila

Response 201

Resumen agregado más el detalle por fila. index es la posición (base 0) dentro del array enviado.

{
  "created": 1,
  "failed": 1,
  "results": [
    {
      "index": 0,
      "ok": true,
      "product": { "uuid": "p1q2r3...", "slug": "camiseta-de-algodon", "title": "Camiseta de algodón", "price": 19.99 },
      "warning": "No se pudo descargar la imagen"
    },
    { "index": 1, "ok": false, "error": "price: Precio inválido" }
  ]
}

El status HTTP siempre es 201 mientras la petición se procese: el éxito o fracaso de cada fila se reporta dentro de results. Revisa failed y los error/warning por fila para mostrar el resultado al vendedor.

Errores

CódigoDescripción
401No autenticado
404Tienda no encontrada o no es del usuario
400Body inválido (falta shop_uuid, products vacío o con más de 100 filas)
429Demasiadas solicitudes (rate limit)

GET /market/products?shop={uuid}

Lista los productos de una tienda propia (incluye inactivos; oculta los eliminados).

Autenticación

Bearer Token. Solo el dueño de la tienda.

Query params

ParámetroTipoRequeridoDescripción
shopstring (uuid)SíUUID de la tienda
pagenumberNoPágina (default 1)
takenumberNoItems por página (default 50, máx 100)

Response

{ "products": [ { "uuid": "...", "title": "...", "price": 19.99, "stock": 50, "active": true } ], "total": 1, "page": 1, "take": 50 }

PATCH /market/products/{uuid}

Edita un producto propio. El kind es inmutable.

Autenticación

Bearer Token. Solo el dueño (vía la tienda).

Parámetros (body)

Todos opcionales: title (3-300), description (≤5000), price (>0), currency, stock (≥0 o null), track_inventory, digital_payload, main_image, images, category, sku, ship_to, active, featured. Si track_inventory se pone en false, el stock pasa a null (ilimitado). Los ejes de opciones (option_axes) no se editan aquí: usa PUT /market/products/{uuid}/variants.

Response

{ "product": { "uuid": "...", "title": "...", "..." : "..." } }

DELETE /market/products/{uuid}

Baja lógica del producto (soft delete). Deja de aparecer en la tienda.

Autenticación

Bearer Token. Solo el dueño.

Response

{ "success": true }

Errores

CódigoDescripción
404Producto no encontrado o no es del usuario