QvaPay API
Marketplace

Comprar

Crear una orden de compra. Descuenta el saldo del comprador y acredita al vendedor de forma atómica.

POST /market/order

Compra un producto. La operación es atómica: valida inventario y saldo, descuenta el total al comprador, acredita el neto al vendedor y la comisión a la plataforma, y crea la orden. Comprar es abierto para cualquier usuario autenticado.

El comprador paga el precio publicado (precio × cantidad); la comisión se descuenta del vendedor (ver modelo de comisión).

Autenticación

Bearer Token. Rate limit: 5 req/20s por usuario. No puedes comprar tu propio producto.

Request — producto digital o servicio

curl -X POST https://api.qvapay.com/market/order \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "product_uuid": "p1q2r3...", "quantity": 1 }'

Request — producto con variantes

Si el producto define variantes (Color, Talla…), variant_uuid es obligatorio; sin él la API responde 400 con code: "VARIANT_REQUIRED". Las variantes disponibles se ven en la vitrina pública o vía GET /market/products/{uuid}/variants (dueño). El precio cobrado es el de la variante (o el del producto si la variante no define uno) y el stock se descuenta por variante.

curl -X POST https://api.qvapay.com/market/order \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "product_uuid": "p1q2r3...", "variant_uuid": "v1a2...", "quantity": 1 }'

Request — producto físico

Los productos físicos requieren una dirección de envío estructurada (internacional): una dirección guardada (shipping_address_id) o una nueva (new_address). Si el producto define destinos (ship_to), la dirección debe estar dentro de ellos; si no, la API responde 400 con code: "SHIP_TO_BLOCKED".

Además, la tienda debe tener zonas de envío configuradas: la zona que cubra el destino determina el costo de la mensajería (shipping_amount, que se suma al total y va íntegro al vendedor, sin comisión) y el plazo estimado. Sin zonas configuradas la API responde 409 con code: "SHIPPING_NOT_CONFIGURED"; si ninguna zona cubre el destino, 400 con code: "NO_SHIPPING_ZONE". Una zona puede ofrecer envío gratis a partir de cierto subtotal.

Si varias zonas de la tienda cubren la dirección (por ejemplo, zonas por reparto o municipio dentro de una misma provincia), el comprador debe indicar cuál en shipping_zone_id (el id de la zona en shop.shipping.zones); sin ella la API responde 400 con code: "SHIPPING_ZONE_REQUIRED". Con una sola zona aplicable el campo es opcional.

curl -X POST https://api.qvapay.com/market/order \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "product_uuid": "p1q2r3...",
    "quantity": 2,
    "new_address": {
      "recipient_name": "John Doe",
      "line1": "1600 Amphitheatre Pkwy",
      "city": "Mountain View",
      "state": "CA",
      "postal_code": "94043",
      "phone": "+13105551234",
      "save_as_default": true
    },
    "note": "Dejar en recepción"
  }'

Parámetros (body)

ParámetroTipoRequeridoDescripción
product_uuidstringSíUUID del producto
variant_uuidstringCondicionalUUID de la variante. Requerido si el producto tiene variantes activas
quantitynumber (≥1, ≤999)No (default 1)Cantidad
shipping_address_idstring (uuid)CondicionalDirección guardada. Requerido para físicos si no se envía new_address
shipping_zone_idstringCondicionalZona de envío elegida. Requerido para físicos cuando varias zonas de la tienda cubren la dirección
new_addressobjectCondicionalDirección nueva (se guarda en el perfil). Ver campos abajo
notestring (≤1200)NoInstrucciones de entrega (opcional)
idempotency_keystring (8–64, [A-Za-z0-9._-])NoClave de idempotencia por intento de compra: un reintento con la misma clave devuelve la orden ya creada (200 con duplicate: true) en vez de duplicarla
cart_keystring (8–64, [A-Za-z0-9._-])NoBatch de carrito: las órdenes que comparten cart_key cobran el envío una sola vez por tienda (lo cobra la primera orden física del batch; ventana de 1h)

new_address (productos físicos): recipient_name (req), line1 (req), city (req), state (req, ≤64), postal_code, line2, phone, label, country (ISO-2, default US), save_as_default (boolean). Reglas por país: con country: "US", state debe ser un estado USPS de 2 letras y postal_code un ZIP válido (requerido); con country: "CU", state debe ser un código de provincia de 3 letras (LHA, MTZ…) y postal_code es opcional; en otros países state es texto libre y postal_code opcional.

Response 201

{
  "order": {
    "uuid": "o1r2d3...",
    "status": "paid",
    "total": 44.98,
    "gift_card_amount": 0,
    "shipping_amount": 5.00,
    "cash_paid": 44.98,
    "quantity": 2
  },
  "transaction_uuid": "a1b2c3d4-..."
}
  • total = subtotal del producto + shipping_amount. cash_paid es lo debitado del saldo (total − gift card aplicada).
  • Los productos digitales quedan en status: "fulfilled" de inmediato; el entregable (digital_payload) se consulta en el detalle de la compra.
  • Físico/servicio quedan en status: "paid" para que el vendedor los gestione.

Errores

CódigoDescripción
400Saldo insuficiente
400Falta dirección de envío para un producto físico
400El producto requiere elegir una variante (code: "VARIANT_REQUIRED")
400El vendedor no envía a ese destino (code: "SHIP_TO_BLOCKED")
400La tienda no tiene una zona de envío para ese destino (code: "NO_SHIPPING_ZONE")
400Varias zonas cubren la dirección y falta shipping_zone_id (o no cubre la dirección) (code: "SHIPPING_ZONE_REQUIRED")
409La tienda aún no configuró sus envíos (code: "SHIPPING_NOT_CONFIGURED")
400No puedes comprar tu propio producto
400La tienda no está activa
400Dirección de envío no encontrada
404Producto o variante no disponible
409No hay stock suficiente
429Rate limit excedido
500Error al procesar la compra