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ámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
shop_uuid | string | Sí | UUID de la tienda propia |
title | string (3-300) | Sí | Título |
description | string (≤5000) | No | Descripción |
kind | physical | digital | service | No (default physical) | Tipo |
price | number (>0, ≤100000) | Sí | Precio en USD |
currency | string | No (default USD) | Moneda |
stock | number (≥0) | null | No | Inventario. null = ilimitado |
track_inventory | boolean | No (default true) | Si es false, el stock pasa a ilimitado |
digital_payload | string (≤20000) | No | Entregable (enlace/clave) — solo kind: digital |
main_image | string (URL) | No | Imagen principal |
images | string[] (≤12) | No | Imágenes adicionales |
category | number | No | Categoría interna |
sku | string (≤64) | No | Código interno del vendedor. Letras, números y . _ -. Único por tienda |
ship_to | object | null | No | Destinos de envío (solo aplica a kind: physical). null u omitido = envía a cualquier destino. Ver formato abajo |
active | boolean | No (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ódigo | Descripción |
|---|---|
401 | No autenticado |
404 | Tienda no encontrada o no es del usuario |
400 | Datos inválidos |
409 | SKU 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ámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
option_axes | string[] (≤3) | Sí | Ejes de opciones (ej. ["Color","Talla"]), sin duplicados |
variants | object[] (≤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ódigo | Descripción |
|---|---|
400 | Datos inválidos (ejes/combinaciones duplicados, opciones que no cubren los ejes, SKU repetido en el payload) |
404 | Producto no encontrado o no es del usuario |
409 | SKU 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ámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
shop_uuid | string | Sí | UUID de la tienda propia |
products | object[] (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ámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
image_url | string (URL https) | No | El 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ódigo | Descripción |
|---|---|
401 | No autenticado |
404 | Tienda no encontrada o no es del usuario |
400 | Body inválido (falta shop_uuid, products vacío o con más de 100 filas) |
429 | Demasiadas 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ámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
shop | string (uuid) | Sí | UUID de la tienda |
page | number | No | Página (default 1) |
take | number | No | Items 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ódigo | Descripción |
|---|---|
404 | Producto no encontrado o no es del usuario |