Documentación API v1
Introducción
La Vendly API v1 te permite integrar el catálogo, pedidos, clientes y métricas de tu tienda con cualquier aplicación externa: e-commerce, app móvil, ERP, chatbots, y más.
Base URL: https://appvendly.lat/api/v1/
- Todos los endpoints devuelven JSON con
"ok": trueen caso de éxito. - En caso de error devuelven
"ok": falsey un campo"error"descriptivo. - Los montos son números en la moneda del país del negocio: Paraguay → PYG (guaraníes, sin decimales), Argentina → ARS (pesos, hasta 2 decimales). Podés consultarla en
GET /api/v1/config. - Los precios de los pedidos siempre se toman del catálogo de Vendly: no hace falta (ni se usa) enviar precios ni totales.
¿Qué parte de la API uso?
| Quiero… | Uso | ¿Dónde va el código? |
|---|---|---|
| Mostrar mi catálogo y recibir pedidos en mi página web | Tienda web — /api/public/… sin clave |
Puede ir en el navegador (HTML / JavaScript de tu web) |
| Ver clientes, ventas, métricas o stock; crear pedidos con datos del cliente, variantes o precio mayorista; cambiar el estado de pedidos; sincronizar con otro sistema | API v1 — /api/v1/… con API Key |
Solo en tu servidor (backend). Nunca en la página web. |
Cómo obtener tu API Key
El cliente no puede generarlas desde su propio panel.
Pasos para obtener tu key
- Solicitá tu API Key por WhatsApp 0971 994 032 o desde appvendly.lat
- El equipo la genera desde el panel administrativo y te la entrega
- Guardala como una contraseña: solo en tu servidor, en una variable de entorno (por ejemplo
VENDLY_API_KEY). No se puede recuperar si se pierde, solo regenerar. - Tu servidor la manda en el header
X-API-Keyde cada request a/api/v1
Características de la key
- Formato: hash hexadecimal de 64 caracteres
- Intransferible: vinculada exclusivamente a tu cuenta Vendly
- Revocable: puede desactivarse en cualquier momento
- Monitoreable: se registra la fecha del último uso y la cantidad de requests
Contacto
Seguridad: cómo proteger tu API Key
Tu API Key es como la contraseña de tu negocio.
Con ella se pueden ver todos tus clientes (nombre, teléfono, email, RUC/CI, dirección), tus ventas y métricas, crear pedidos que descuentan stock y cancelar pedidos. Quien la tenga puede hacerlo desde cualquier computadora.Por qué no puede ir en tu página web
Todo lo que forma parte de una página web (HTML, archivos .js, configuración de React/Vue/Next, etc.) se descarga en el navegador de cada visitante.
Cualquiera puede verlo con clic derecho → Ver código fuente o con la tecla F12. Si la clave está ahí — aunque esté "escondida" en un archivo, minificada o en una variable — es pública.
✗ Nunca
- Poner la clave en el HTML o en archivos
.jsde tu web - Variables públicas de frameworks (
NEXT_PUBLIC_…,VITE_…,REACT_APP_…): terminan en el navegador - Llamar a
/api/v1desde el navegador - Subir la clave a GitHub u otro repositorio
- Mandarla en la URL (
?api_key=…): queda en historiales y registros
✓ Siempre
- Guardar la clave en una variable de entorno de tu servidor (
VENDLY_API_KEY) - Llamar a
/api/v1solo desde tu backend (Node, PHP, Python, Laravel, una función serverless, etc.) - Para mostrar productos y recibir pedidos en tu web, usar la Tienda web sin clave
- Si tu web necesita datos privados, que llame a tu servidor y tu servidor llame a Vendly
Cómo se conecta todo
Tu página web (navegador del cliente) │ ├── Catálogo y pedido simple ──────────► appvendly.lat/api/public/{tenant_id}/… (sin clave) │ └── Pedido con datos del cliente ──────► TU servidor ──► appvendly.lat/api/v1/pedidos / variantes / mayorista (tiene la clave X-API-Key: process.env.VENDLY_API_KEY en variable de entorno)
Ejemplo: tu servidor como intermediario (Node.js + Express)
// .env de TU servidor (nunca se sube al repositorio ni a la web) VENDLY_API_KEY=tu-clave-de-64-caracteres // server.js de TU servidor app.post('/checkout', async (req, res) => { // 1. Tu web le manda a TU servidor los productos elegidos y los datos del cliente const { items, customer } = req.body; // 2. Tu servidor crea el pedido en Vendly con la clave (el navegador nunca la ve) const r = await fetch('https://appvendly.lat/api/v1/pedidos', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.VENDLY_API_KEY }, body: JSON.stringify({ items, customer }) }); // 3. A la web solo se le devuelve lo necesario const { ok, pedido, error } = await r.json(); res.status(ok ? 200 : 400).json(ok ? { ticket: pedido.ticket_number, total: pedido.total } : { error }); });
Si tu clave quedó expuesta
- Avisanos por WhatsApp: la desactivamos al momento y te generamos una nueva.
- Sacala del código de tu web y del repositorio.
- Configurá la nueva solo en las variables de entorno de tu servidor.
Autenticación
Todos los endpoints /api/v1 requieren el header X-API-Key con tu clave activa. Estas llamadas se hacen solo desde tu servidor.
process.env.VENDLY_API_KEY. No copies estos ejemplos en el código de tu página web.Header requerido
X-API-Key: <tu-clave-de-64-caracteres>
Ejemplo Node.js (en tu servidor)
const res = await fetch('https://appvendly.lat/api/v1/metricas', { headers: { 'X-API-Key': process.env.VENDLY_API_KEY } }); const data = await res.json(); console.log(data);
Ejemplo cURL (terminal / servidor)
curl -H "X-API-Key: $VENDLY_API_KEY" https://appvendly.lat/api/v1/metricas
Códigos de error
| HTTP | Significado | Causa frecuente |
|---|---|---|
200 | OK | Solicitud exitosa |
400 | Bad Request | Faltan campos obligatorios o datos inválidos |
401 | Unauthorized | Header X-API-Key ausente, inválido o inactivo |
404 | Not Found | Recurso no encontrado |
500 | Server Error | Error interno — reportar al soporte |
Formato de error
{ "ok": false, "error": "Descripción del error" }
Catálogo de productos
Parámetros query
| Parámetro | Tipo | Descripción |
|---|---|---|
page | integer | Opcional Página (default: 1) |
limit | integer | Opcional Items por página, máx. 100 (default: 50) |
categoria | string | Opcional Filtrar por categoría exacta |
incluir_sin_stock | boolean | Opcional true para incluir también los productos sin stock (para mostrarlos como "agotados") |
Node.js (en tu servidor)
const res = await fetch('https://appvendly.lat/api/v1/catalogo?limit=20&page=1', { headers: { 'X-API-Key': process.env.VENDLY_API_KEY } }); const { productos, total } = await res.json();
cURL
curl -H "X-API-Key: $VENDLY_API_KEY" \ "https://appvendly.lat/api/v1/catalogo?categoria=Electrónica"
Respuesta
{
"ok": true,
"productos": [
{
"id": "uuid",
"name": "Auriculares Bluetooth",
"description": "...",
"price": 145000,
"wholesale_price": 120000,
"stock": 8,
"unit": "Unid.",
"category": "Electrónica",
"image_url": "https://appvendly.lat/uploads/img.jpg", // URL absoluta
"images": [],
"variants": [// tipo: "color" o "modelo" (talles, diseños...)
{ "id": "uuid-variante", "color": "Negro", "stock": 5, "tipo": "color", "image_url": null },
{ "id": "uuid-variante", "color": "Talle M", "stock": 3, "tipo": "modelo", "image_url": null }
]
}
],
"total": 47, "page": 1, "limit": 20
}
Node.js (en tu servidor)
const res = await fetch(`https://appvendly.lat/api/v1/catalogo/${productId}`, { headers: { 'X-API-Key': process.env.VENDLY_API_KEY } }); const { producto } = await res.json();
Respuesta
{ "ok": true, "producto": { /* mismo esquema que el array de /catalogo */ } }
Parámetros query
| Parámetro | Tipo | Descripción |
|---|---|---|
bajo | boolean | Opcional true para devolver solo productos con stock ≤ mínimo |
Respuesta
{
"ok": true,
"stock": [
{ "id": "uuid", "name": "Producto", "code": "P001",
"category": "Bazar", "stock": 3, "min_stock": 5, "unit": "Unid.", "active": true }
]
}
Pedidos
Respuesta
{
"ok": true,
"pedidos": [
{
"id": "uuid", "ticket_number": "WEB-123456",
"total": 120000, "estado": "pendiente", // pendiente | pagado | cancelado
"status": "pending",
"payment_method": "efectivo",
"cliente": "Juan Pérez", "cliente_phone": "0981000000", // "Consumidor Final" si no hay cliente
"customer_note": "Entregar por la tarde",
"created_at": "2026-07-05T10:30:00Z"
}
]
}
Respuesta
{
"ok": true,
"pedido": {
"id": "uuid", "ticket_number": "WEB-123456", "total": 90000,
"estado": "pendiente", "payment_method": "transferencia", "cliente": "Juan Pérez",
"items": [
{ "product_id": "uuid", "product_name": "Remera — Negro", "variant_id": "uuid-variante",
"variante": "Negro", "quantity": 2, "price": 45000, "subtotal": 90000 }
]
}
}
unit_price o total se ignoran. El total devuelto se redondea según la moneda (PYG sin decimales, ARS con 2).
Body JSON
| Campo | Tipo | Descripción |
|---|---|---|
items | array | Requerido Entre 1 y 200 ítems: [{ product_id, quantity, variant_id }]. quantity mayor a 0 y hasta 10000. |
items[].variant_id | string | Requerido si el producto tiene variantes id de la variante (color o modelo/talle) tal como viene en /catalogo. En su lugar se puede mandar variante con el nombre exacto (ej. "Negro"). Se descuenta el stock de esa variante; la cantidad debe ser entera. |
price_type | string | Opcional "minorista" (default) o "mayorista". Con mayorista se usa el precio mayorista de cada producto; si un producto no lo tiene cargado, se usa su precio de venta. |
payment_method | string | Opcional default: efectivo |
customer_note | string | Opcional Nota del cliente |
customer | object | Opcional { name, phone, email } |
Node.js (en tu servidor)
const res = await fetch('https://appvendly.lat/api/v1/pedidos', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.VENDLY_API_KEY }, body: JSON.stringify({ items: [ { product_id: 'uuid-producto', quantity: 2, variant_id: 'uuid-variante' }, // producto con variantes { product_id: 'uuid-otro', quantity: 1 } // producto sin variantes ], price_type: 'minorista', payment_method: 'transferencia', customer: { name: 'Juan Pérez', phone: '0981000000' } }) }); const { ok, pedido } = await res.json(); // pedido.total = total calculado por Vendly
cURL
curl -X POST https://appvendly.lat/api/v1/pedidos \ -H "Content-Type: application/json" \ -H "X-API-Key: $VENDLY_API_KEY" \ -d '{"items":[{"product_id":"uuid","quantity":1}]}'
Respuesta exitosa
{
"ok": true,
"pedido": {
"id": "uuid", "ticket_number": "WEB-123456", "total": 90000,
"estado": "pendiente", "price_type": "minorista",
"items": [
{ "product_id": "uuid", "product_name": "Remera — Negro", "variante": "Negro",
"quantity": 2, "price": 45000, "subtotal": 90000, "precio": "minorista" }
]
}
}
Errores
| HTTP | error | Causa |
|---|---|---|
400 | Sin productos | items vacío o con más de 200 ítems |
400 | Artículo inválido en el pedido | Falta product_id o quantity no es válida |
400 | Producto … no encontrado | El product_id no existe o está inactivo |
400 | Stock insuficiente: "…" (disponible: N) | No hay stock del producto o de la variante pedida |
400 | El producto "…" tiene variantes: indicá variant_id o variante | Falta indicar el color o modelo |
400 | La variante "…" no existe en "…" | El variant_id o nombre no corresponde a ese producto |
400 | price_type debe ser "minorista" o "mayorista" | Valor de price_type no válido |
| Ante cualquier error no se crea el pedido ni se descuenta stock. | ||
Body JSON
| Campo | Tipo | Descripción |
|---|---|---|
estado | string | Requerido "pagado" o "cancelado" |
payment_method | string | Opcional Al marcar pagado, actualiza el método de pago (ej. "transferencia") |
Node.js (en tu servidor)
const res = await fetch(`https://appvendly.lat/api/v1/pedidos/${pedidoId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'X-API-Key': process.env.VENDLY_API_KEY }, body: JSON.stringify({ estado: 'cancelado' }) });
Respuesta
{ "ok": true, "pedido": { "id": "uuid", "ticket_number": "WEB-123456", "estado": "cancelado", "stock_devuelto": true } }
Errores
| HTTP | error |
|---|---|
400 | estado debe ser "pagado" o "cancelado" |
400 | El pedido ya está cancelado |
404 | Pedido no encontrado (no existe, no es un pedido web o es de otro negocio) |
También se puede hacer desde el panel de Vendly: en los pedidos web pendientes, Rechazar; o en cualquier venta, Anular. En ambos casos el stock vuelve solo.
Clientes
Parámetros query
| Parámetro | Tipo | Descripción |
|---|---|---|
page | integer | Opcional Página (default: 1) |
limit | integer | Opcional máx. 100 (default: 50) |
Respuesta
{
"ok": true,
"clientes": [
{ "id": "uuid", "name": "Cliente Ejemplo",
"phone": "0981000000", "email": "c@mail.com", "ruc_ci": "1234567",
"address": "Av. Siempre Viva 123", "created_at": "2026-07-05T10:30:00Z" }
],
"total": 152, "page": 1, "limit": 50
}
Node.js (en tu servidor)
const res = await fetch(`https://appvendly.lat/api/v1/clientes/${clienteId}`, { headers: { 'X-API-Key': process.env.VENDLY_API_KEY } });
Respuesta
{ "ok": true, "cliente": { "id": "uuid", "name": "...", "address": "...", /* ... */ } }
Ventas
Parámetros
| Parámetro | Tipo | Descripción |
|---|---|---|
limit | integer | Opcional máx. 500 (default: 100) |
Respuesta
{
"ok": true,
"ventas": [
{ "id": "uuid", "ticket_number": "T-0001", "total": 85000,
"payment_method": "efectivo", "status": "completed",
"channel": "pos", "cliente": "Juan Pérez",
"created_at": "2026-07-05T10:00:00Z" }
]
}
Respuesta
{
"ok": true,
"total_hoy": 350000,
"ventas_hoy": 4,
"total_mes": 8500000,
"ventas_mes": 87,
"ticket_promedio": 97701.15 // promedio histórico de todas las ventas
}
Métricas & Configuración
Respuesta
{
"ok": true,
"productos_activos": 48,
"clientes_total": 152,
"ventas_mes": 8500000,
"ventas_mes_count": 87,
"stock_bajo": 3
}
Respuesta
{
"ok": true,
"config": { "currency": "PYG", "country": "PY", "usd_rate": 7800, "ars_rate": 9.5 }
}
currency es PYG para negocios de Paraguay y ARS para negocios de Argentina. Todos los montos de la API están en esa moneda.
Tienda web: catálogo y pedidos sin clave
Base URL: https://appvendly.lat/api/public/{tenant_id}/
{tenant_id}es el identificador público de tu tienda (te lo pasa el equipo de Vendly). No es secreto: puede ir en tu web.- No mandes el header
X-API-Keyen estas llamadas: no hace falta y la dejarías expuesta. - Permiten llamadas desde cualquier dominio (CORS) y tienen un límite de 60 consultas por minuto por visitante (si se supera responden
429). - Solo aparecen los productos activos y marcados como "Publicar en web" en Vendly.
Parámetros query
| Parámetro | Tipo | Descripción |
|---|---|---|
page | integer | Opcional Página (default: 1) |
limit | integer | Opcional Items por página, máx. 100 (default: 20) |
JavaScript (en tu página web)
const TIENDA = 'tu-tenant-id'; // público, no es la API Key const res = await fetch(`https://appvendly.lat/api/public/${TIENDA}/productos?limit=50`); const { productos, total } = await res.json();
Respuesta
{
"ok": true,
"productos": [{
"id": "uuid", "name": "Remera", "description": "...", "category": "Indumentaria",
"price": 50000, "precio_minorista": 50000, "precio_mayorista": 40000, "stock": 8,
"image_url": "https://appvendly.lat/uploads/remera.jpg", "images": ["https://…"],
"variants": [{ "color": "Negro", "stock": 5 }]
}],
"total": 47, "page": 1, "limit": 50
}
También: GET /api/public/{tenant_id}/productos/{id} (un producto) y GET /api/public/{tenant_id}/categorias.
Este listado incluye productos sin stock: mostralos como "Agotado" usando el campo stock.
Respuesta
{ "ok": true, "currency": "PYG", "country": "PY", "usd_rate": 7800, "ars_rate": 9.5 }
currency: PYG (Paraguay, sin decimales) o ARS (Argentina, hasta 2 decimales).
Body JSON
| Campo | Tipo | Descripción |
|---|---|---|
items | array | Requerido [{ product_id, quantity }], quantity mayor a 0 |
payment_method | string | Opcional default: transferencia |
customer_note | string | Opcional Nombre, teléfono, dirección y aclaraciones del cliente (texto libre) |
JavaScript (en tu página web)
const res = await fetch(`https://appvendly.lat/api/public/${TIENDA}/orders`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, // sin X-API-Key body: JSON.stringify({ items: [{ product_id: 'uuid-producto', quantity: 2 }], payment_method: 'transferencia', customer_note: 'Juan Pérez · 0981 000 000 · Av. España 123, Asunción' }) }); const { success, order_id, error } = await res.json();
Respuesta
{ "success": true, "order_id": "uuid" } // error: { "success": false, "error": "Producto no disponible" }