API Pública de Vendty (1.0.0)

Download OpenAPI specification:

Introducción

Estado actual: <strong>solo lectura</strong>

Introducción

API pública de Vendty para integraciones de lectura: ERPs, sistemas contables, dashboards externos y aplicaciones a medida que necesitan consultar datos del negocio. REST · JSON · OAuth2 (Personal Access Tokens con scopes).

Estado actual: solo lectura

  • URL base de producción: https://api-pub.vendty.com/v1
  • Productos: catálogo completo con filtros, búsqueda y sincronización incremental.
  • Categorías: lista del negocio.
  • Clientes: terceros del negocio (NIT, datos de contacto), grupos, historial de ventas.
  • Ventas: lista paginada con filtros (estado, fecha, cliente, almacén, etc.) y detalle con líneas + pagos.
  • Notas crédito: lectura de NCs asociadas a una venta.
  • Métodos de pago: catálogo del negocio.
  • Restaurante (solo para tenants tipo_negocio='restaurante'): pisos, mesas con estado computado en vivo, items pedidos con adiciones y variations resueltas a nombres, kitchen tracking opt-in vía Comanda Virtual.
  • Quick Service: órdenes de Quick Service (zona = -1) agrupadas por mesa virtual.
  • Catálogos (catalogs:read): almacenes, impuestos, vendedores, usuarios y listas de precio.
  • Compras (purchases:read): proveedores, facturas de compra (con líneas y pagos), documentos soporte DIAN.
  • Reports async (reports:read): listado y descarga de reports generados desde systempos (vía S3).
  • Cotizaciones / Quotes (quotes:read): proformas (en cafeteriavendty se usa más como borrador de gastos).
  • Cajas registradoras (cash-registers:read): cajas físicas + cierres diarios con totales.
  • Webhooks (webhooks:manage): suscripción a 3 eventos disparados por scanner cada minuto: sale.created, sale.voided, table_order.created. Firma HMAC-SHA256 estilo Stripe.
  • Identidad del cliente API (/v1/me) y health check público (/v1/health).

Nota importante. La API pública actualmente no acepta escrituras (POST/PATCH/DELETE) por decisión de dirección. Si tu integración necesita crear, actualizar o anular datos en Vendty, hablamos directamente para evaluar tu caso de uso. El código y los endpoints están preservados para una reactivación futura controlada.

Lo que ofrece

Seguridad por defecto

  • HTTPS obligatorio, HSTS, IDs opacos (sin enumeración secuencial).
  • Límites de uso por cliente con planes (Básico / Pro / Empresarial).
  • Auditoría de todas las llamadas: tanto las exitosas como las fallidas.
  • Usuarios MySQL solo de lectura por negocio — un token de lectura filtrado no puede modificar nada.

Contrato estable

Una vez publicada una versión bajo /v1/, el contrato se preserva: no se eliminan endpoints, no se quitan campos, no se cambian tipos. Los cambios incompatibles van a /v2/.

Pensada para integraciones reales

  • Paginación amistosa para colecciones acotadas (per_page hasta 200, links.next para iterar).
  • Cursor pagination para colecciones grandes como /v1/sales (escala a tenants con millones de ventas).
  • Sincronización incremental con ?updated_since en /v1/products.
  • Filtros poderosos en ventas: rango de fechas (max 90 días), cliente, almacén, vendedor, estado, número de factura.

Soporte

Si tienes problemas con tu integración: incluye siempre el header X-Request-Id de la respuesta cuando contactes a tu representante de Vendty. Con ese ID ubicamos la llamada exacta en menos de un minuto.

Inicio rápido — API Pública de Vendty v1

En 5 minutos haces tu primera llamada autenticada a la API y confirmas que tus credenciales funcionan.

Lo que necesitas

  • Un Bearer token emitido a tu aplicación por Vendty. Si todavía no tienes uno, tu representante de Vendty registra un ApiClient para tu negocio y te entrega un token. Los tokens se muestran una sola vez al emitirlos — guárdalo en un lugar seguro (gestor de contraseñas, vault, no en el repositorio de tu aplicación).
  • curl o cualquier cliente HTTP.

1. Verifica que tu token funciona

curl -s https://api-pub.vendty.com/v1/me \
  -H "Authorization: Bearer TU_TOKEN_AQUI"

Respuesta esperada (HTTP 200):

{
  "data": {
    "id": "8d3b1bff-…",
    "name": "Mi aplicación",
    "rate_tier": "basic",
    "scopes": [],
    "allowed_origins": [],
    "created_at": "2026-05-11T18:35:00+00:00"
  },
  "meta": { "request_id": "f7ad…" }
}

Si recibes 401 unauthenticated → el token está mal, expiró o fue revocado. Contacta a Vendty.

Si recibes 403 tenant_not_enabled → tu negocio aún no ha completado el alta para la API. Contacta a Vendty.

2. Revisa tus permisos (scopes)

El campo data.scopes te dice qué puede hacer tu token. Catálogo completo de lectura activos:

Scope Permite
products:read Productos + categorías
customers:read Clientes + grupos + historial de ventas por cliente
sales:read Ventas (lista + detalle) + métodos de pago
credit-notes:read Notas crédito asociadas a una venta
restaurant:read Pisos, mesas y órdenes de mesa + Quick Service
catalogs:read Almacenes, impuestos, vendedores, usuarios, listas de precio
purchases:read Proveedores, facturas de compra, documentos soporte DIAN
quotes:read Cotizaciones / Proformas
cash-registers:read Cajas registradoras + cierres diarios con totales
reports:read Reportes async generados desde systempos (descarga vía S3)
webhooks:manage CRUD de webhooks + test + auditoría de deliveries

Si el arreglo data.scopes viene vacío, tu token solo puede llamar a /v1/me y /v1/health. Para usar endpoints de negocio pídele a Vendty un token con los scopes que necesitas.

3. Lee los headers de cada respuesta

Todas las respuestas incluyen:

Header Significado
X-Request-Id Identificador único de esta petición — úsalo al reportar incidencias
X-RateLimit-Limit Límite por minuto de tu plan
X-RateLimit-Remaining Cuántas llamadas te quedan en la ventana actual
X-RateLimit-Reset Marca de tiempo (Unix) en que se reinicia el contador

4. Esta API es solo lectura

Por decisión de dirección, la API pública actualmente solo expone endpoints GET. Cualquier POST/PATCH/DELETE retorna 404 route_not_found.

Si tu integración necesita crear o actualizar datos en Vendty (clientes, ventas, anulaciones, etc.), coordina con tu representante de Vendty — esa funcionalidad existe en el código y se puede habilitar por tenant cuando se requiera, bajo evaluación caso por caso.

5. Paginación: dos modos según el endpoint

Las colecciones acotadas (/v1/products, /v1/categories, /v1/customers, /v1/customer-groups, /v1/payment-methods) usan paginación offset clásica: el meta incluye current_page, last_page, per_page, from, to y total. Salta páginas con ?page=N. Cap máximo per_page=200.

Las colecciones que pueden tener millones de filas (/v1/sales, /v1/customers/{id}/sales) usan paginación por cursor — diseñada para escalar sin recalcular COUNT(*) en cada request:

  • El meta incluye next_cursor (opaco) y prev_cursor. NO incluye total ni last_page.
  • Para iterar la colección completa, sigue links.next hasta que sea null.
  • Cap máximo per_page=100 (no 200 — más conservador).
# Primera página
curl "https://api-pub.vendty.com/v1/sales?per_page=50&date_from=2026-01-01&date_to=2026-03-31" \
  -H "Authorization: Bearer $TOKEN"

# El response trae: { "data": [...], "meta": { "next_cursor": "eyJmZWNoY...", ... }, "links": { "next": "https://..." } }

# Página siguiente — usa la URL completa de links.next, NO armes la query manualmente
curl "https://api-pub.vendty.com/v1/sales?per_page=50&date_from=...&cursor=eyJmZWNoY..." \
  -H "Authorization: Bearer $TOKEN"

# Cuando links.next sea null, terminaste.

Por qué cursor: un tenant con 6 millones de ventas haría SELECT COUNT(*) cada request en paginación offset — segundos perdidos por cada página. Cursor solo hace WHERE (fecha, id) < (?, ?) LIMIT 50 que aprovecha índice y escala a cualquier tamaño.

6. Errores

Todos los errores siguen el mismo formato:

{
  "error": {
    "code": "validation_failed",
    "message": "La información proporcionada no es válida.",
    "details": { "fields": { "email": ["El campo email debe ser una dirección válida."] } },
    "request_id": "f7ad…"
  }
}

Programa tu lógica contra error.code (estable, legible por máquina). El campo error.message es para humanos y puede cambiar.

Siguientes pasos

Autenticación — v1

Bearer tokens

Toda llamada a /v1/* (excepto /v1/health) debe llevar un token Bearer de Passport en el header Authorization:

Authorization: Bearer tu_token

Los tokens los emite Vendty — no hay endpoint público de registro ni de login. Para obtener un token, contacta a tu representante de Vendty.

Propiedades del token

Un token tiene:

  • Un cliente — la aplicación (ApiClient) a la que pertenece.
  • Una etiqueta — nombre legible (ej. "App móvil prod 2026" o "Sync contabilidad 2026") para identificarlo en los logs de auditoría.
  • Scopes — qué puede hacer (ver más abajo).
  • Una fecha de expiración — normalmente 365 días. Los tokens expirados devuelven 401 unauthenticated.

Puedes inspeccionar tu token activo con GET /v1/me.

Scopes (permisos)

Los tokens llevan cero o más scopes del catálogo. Por defecto un token no trae ningún scope — cada endpoint exige al menos uno. Pedir un scope que no tienes devuelve 403 scope_missing.

Scope Endpoints
products:read GET /v1/products, GET /v1/products/{id}, GET /v1/categories
customers:read GET /v1/customers, GET /v1/customers/{id}, GET /v1/customer-groups, GET /v1/customers/{id}/sales
sales:read GET /v1/sales, GET /v1/sales/{id}, GET /v1/payment-methods
credit-notes:read GET /v1/sales/{id}/credit-notes
restaurant:read GET /v1/restaurant/sections, GET /v1/restaurant/tables, GET /v1/restaurant/tables/{id}/order, GET /v1/quick-service/orders
catalogs:read GET /v1/warehouses, GET /v1/taxes, GET /v1/sellers, GET /v1/users, GET /v1/price-lists
purchases:read GET /v1/suppliers, GET /v1/purchase-orders, GET /v1/purchase-orders/{id}, GET /v1/support-documents, GET /v1/support-documents/{id}
quotes:read GET /v1/quotes, GET /v1/quotes/{id}
cash-registers:read GET /v1/cash-registers, GET /v1/cash-register-closures, GET /v1/cash-register-closures/{id}
webhooks:manage CRUD /v1/webhooks*, POST /v1/webhooks/{id}/test, GET /v1/webhooks/{id}/deliveries
reports:read GET /v1/reports, GET /v1/reports/{id}

Solo lectura. Los scopes de escritura (*:write, voids:write, webhooks:manage, inventory:*) están definidos en el código pero no se emiten mientras la API sea solo lectura por decisión de dirección. Cuando se reactive el modo escritura, este catálogo se ampliará.

Ciclo de vida del token

Operación Quién Cómo
Crear cliente Operador Vendty php artisan api:client:create (una vez por aplicación)
Emitir token Operador Vendty php artisan api:token:issue — el bearer se muestra una sola vez
Inspeccionar GET /v1/me
Revocar Operador Vendty php artisan api:token:revoke
Rotar Tú vía Vendty Pides un token nuevo; el viejo se revoca al entregarte el nuevo

Cómo guardar tu token con seguridad

  • Nunca subas un token al control de versiones (Git). Usa variables de entorno o un gestor de secretos.
  • Nunca lo registres en logs. Si necesitas correlacionar, registra el request_id.
  • Trátalo como una contraseña — si crees que fue filtrado, solicita rotación inmediatamente.

CORS

Si llamas a la API desde un navegador, el origen de tu aplicación debe estar en la lista permitida (allowed_origins) de tu cliente. No se permiten comodines (*). Envía a Vendty los dominios desde los que vas a llamar cuando pidas tu token.

Cuando algo falla

HTTP code Significado
401 unauthenticated Token ausente, mal formado, expirado o revocado
403 scope_missing El token no tiene el scope que ese endpoint requiere
403 client_suspended Tu ApiClient está suspendido — contacta a Vendty
403 tenant_not_enabled Tu negocio aún no está habilitado para la API pública
429 rate_limit_exceeded Bajá la tasa de llamadas — ver header Retry-After

Catálogo de errores — v1

Todas las respuestas no-2xx siguen este formato:

{
  "error": {
    "code": "<string_estable_legible_por_maquina>",
    "message": "<texto_para_humanos>",
    "details": { ... opcional ... },
    "request_id": "<uuid>"
  }
}

error.code es el contrato. Es estable entre versiones. error.message es para humanos y puede cambiar en cualquier momento.

Cómo reaccionar

  • Toma error.code como única fuente de verdad para tu lógica de cliente. Compara contra el string; no parsees error.message.
  • Siempre registra request_id. Cuando contactes a soporte de Vendty, es el dato más útil.
  • Política de reintentos: 429 y 503 son razonables para reintentar con backoff. El resto de 4xx no se reintentan — corrige tu llamada.

Catálogo completo de endpoints — v1

Lista exhaustiva de los 41 endpoints disponibles, con scope requerido, parámetros principales y forma del response. Para schemas detallados (todos los campos de cada recurso), revisa la sección API Reference generada desde el OpenAPI más abajo en este mismo documento.

Convención. Todos los endpoints que toman un {id} lo reciben como ID público opaco (p_…, c_…, s_…, etc.). Nunca uses IDs internos. Detalle de los prefijos en la guía para implementadores.


Sistema (sin scope)

Productos — <code>products:read</code>

Clientes — <code>customers:read</code>

Ventas — <code>sales:read</code>

Notas crédito — <code>credit-notes:read</code>

Restaurante — <code>restaurant:read</code>

Catálogos — <code>catalogs:read</code>

Compras — <code>purchases:read</code>

Cotizaciones — <code>quotes:read</code>

Cajas registradoras — <code>cash-registers:read</code>

Reportes async — <code>reports:read</code>

Webhooks — <code>webhooks:manage</code>

Sistema (sin scope)

Método Path Auth Descripción
GET /v1/health público Health check
GET /v1/me bearer Identidad del cliente API: scopes, rate tier, allowed_origins
curl https://api-pub.vendty.com/v1/health
curl https://api-pub.vendty.com/v1/me -H "Authorization: Bearer $TOKEN"

Productos — products:read

Método Path Descripción
GET /v1/products Lista paginada (offset, per_page ≤ 200)
GET /v1/products/{id} Detalle por p_…
GET /v1/categories Lista de categorías activas

Filtros de /v1/products: search, category_id, warehouse_id (con include=stock), active, include (stock,category), updated_since, sort (name/-name/price/-price/created_at/-created_at).

curl 'https://api-pub.vendty.com/v1/products?search=americano&include=stock,category&warehouse_id=wh_…' \
  -H "Authorization: Bearer $TOKEN"

Clientes — customers:read

Método Path Descripción
GET /v1/customers Lista paginada offset
GET /v1/customers/{id} Detalle de un cliente
GET /v1/customer-groups Grupos de clientes del negocio
GET /v1/customers/{id}/sales Doble scope customers:read + sales:read. Historial de ventas del cliente (cursor pagination)

Filtros de /v1/customers: search (nombre, NIF, email, móvil), group_id, online_store (bool), updated_since, sort.


Ventas — sales:read

Método Path Descripción
GET /v1/sales Lista con cursor pagination (per_page ≤ 100)
GET /v1/sales/{id} Venta + líneas (details) + pagos (payments)
GET /v1/payment-methods Catálogo de métodos de pago del negocio

Filtros de /v1/sales: date_from, date_to (max 90 días), customer_id, seller_id, warehouse_id, status (valid/voided/all), invoice_number, cursor, per_page.

curl 'https://api-pub.vendty.com/v1/sales?date_from=2026-04-01&date_to=2026-05-01&status=valid' \
  -H "Authorization: Bearer $TOKEN"

Notas crédito — credit-notes:read

Método Path Descripción
GET /v1/sales/{id}/credit-notes Lista de NCs asociadas a una venta

Restaurante — restaurant:read

Solo disponible para tenants con opciones.tipo_negocio='restaurante'. Tenants no-restaurante reciben 403 not_a_restaurant.

Método Path Descripción
GET /v1/restaurant/sections Pisos / áreas
GET /v1/restaurant/tables Mesas con status calculado en vivo (free/occupied/bill_requested/reserved)
GET /v1/restaurant/tables/{id}/order Orden activa: items, adiciones (con nombres), variations resueltos, kitchen tracking opcional
GET /v1/quick-service/orders Órdenes Quick Service (zona = -1) agrupadas por mesa virtual

Filtros de /v1/restaurant/tables: section_id, status, warehouse_id.


Catálogos — catalogs:read

Método Path Descripción
GET /v1/warehouses Almacenes / sucursales
GET /v1/taxes Impuestos configurados
GET /v1/sellers Vendedores
GET /v1/users Usuarios del POS (cross-tenant safe: filtrado por db_config_id en BD central)
GET /v1/price-lists Listas de precio

Compras — purchases:read

Método Path Descripción
GET /v1/suppliers Proveedores
GET /v1/purchase-orders Facturas de compra (cursor pagination)
GET /v1/purchase-orders/{id} Detalle: líneas + pagos
GET /v1/support-documents Documentos soporte DIAN (a no obligados a facturar)
GET /v1/support-documents/{id} Detalle de documento soporte

Filtros de /v1/purchase-orders: date_from, date_to, supplier_id, warehouse_id, status, cursor, per_page.


Cotizaciones — quotes:read

Método Path Descripción
GET /v1/quotes Proformas (en cafeteriavendty se usan como borrador de gastos)
GET /v1/quotes/{id} Detalle de una proforma

Filtros: date_from, date_to, supplier_id, warehouse_id.


Cajas registradoras — cash-registers:read

Método Path Descripción
GET /v1/cash-registers Cajas físicas del negocio
GET /v1/cash-register-closures Cierres diarios (incluye cajas abiertas con is_open=true)
GET /v1/cash-register-closures/{id} Detalle de un cierre con totales (income, expenses, closing, count)

Filtros de /v1/cash-register-closures: cash_register_id, warehouse_id, date_from, date_to, is_open (bool).


Reportes async — reports:read

Método Path Descripción
GET /v1/reports Lista de jobs de reporte generados desde systempos
GET /v1/reports/{id} Detalle: estado, progreso, download_url (S3 presigned, expira a 7 días)

Filtros: status (pending/processing/completed/failed), report_type, date_from, date_to.

Solo expone reportes generados por el sistema async (vendty_services.report_jobs). Disparar reportes nuevos vía API requiere endpoint write — está deshabilitado.


Webhooks — webhooks:manage

Método Path Descripción
GET /v1/webhooks Lista los webhooks de tu cliente
POST /v1/webhooks Crea un webhook (devuelve secret UNA vez). Idempotency-Key requerido
GET /v1/webhooks/{id} Detalle (sin secret)
PATCH /v1/webhooks/{id} Actualiza url, events, status. Idempotency-Key requerido
DELETE /v1/webhooks/{id} Elimina (soft delete)
POST /v1/webhooks/{id}/test Encola un delivery de prueba (webhook.test)
GET /v1/webhooks/{id}/deliveries Auditoría de deliveries (cursor pagination, filtros status/after)

Webhooks es el único dominio con verbos POST/PATCH/DELETE habilitados en modo solo-lectura. Se aceptaron porque son meta-operaciones sobre el cliente, no escrituras al negocio.

Eventos disparados por scanner cada minuto: sale.created, sale.voided, table_order.created. Detalle de payload, firma y retries en la página Webhooks.


Resumen rápido

Grupo Endpoints Scope
Sistema 2 (sin scope)
Productos 3 products:read
Clientes 4 customers:read (+ sales:read en uno)
Ventas 3 sales:read
Notas crédito 1 credit-notes:read
Restaurante 4 restaurant:read
Catálogos 5 catalogs:read
Compras 5 purchases:read
Cotizaciones 2 quotes:read
Cajas 3 cash-registers:read
Reportes 2 reports:read
Webhooks 7 webhooks:manage
Total 41 11 scopes

Headers comunes en toda respuesta

Header Descripción
X-Request-Id UUID único por request (úsalo al reportar issues)
X-RateLimit-Limit Límite por minuto del plan
X-RateLimit-Remaining Llamadas restantes en la ventana
X-RateLimit-Reset Unix timestamp del reset
Retry-After Solo en 429, segundos a esperar

Idempotency en mutaciones

Endpoints POST/PATCH/DELETE (hoy solo bajo /v1/webhooks*) requieren header Idempotency-Key (UUID v4). Llamadas con el mismo key dentro de 24h devuelven la respuesta cacheada. Sin el header → 400 idempotency_key_required.

Webhooks — v1

Vendty empuja eventos a tu URL vía webhooks firmados con HMAC-SHA256. Patrón estilo Stripe / Shopify / GitHub: tú expones un endpoint HTTPS, lo registras vía API y nosotros te avisamos cuando algo importante pasa en el negocio del partner.

Estado actual: activos. Aunque la API es de solo lectura para el partner, los eventos sí se generan: un scanner interno corre cada minuto sobre la BD del tenant (venta, ventas_anuladas, orden_producto_restaurant) y dispara los eventos suscritos. Latencia esperada del evento al delivery: 0–60 s + tiempo de retry si tu endpoint falla.

Eventos disponibles

Evento Cuándo se dispara Tabla origen
sale.created Se creó una venta nueva en el POS venta
sale.voided Se anuló una venta ventas_anuladas
table_order.created Se abrió una orden en una mesa de restaurante (excluye Quick Service) orden_producto_restaurant WHERE zona <> -1
webhook.test Disparado manualmente con POST /v1/webhooks/{id}/test. No depende del scanner.

Eventos congelados. credit_note.created, customer.created y customer.updated están definidos en el código pero no se emiten mientras la API sea solo lectura — dependen de POSTs del partner que hoy no existen. Si los necesitas, contacta a Vendty.

Cómo funciona por dentro (resumen)

Cada minuto:
  Scheduler  →  webhooks:scan  →  Por cada (tenant, evento) suscrito:
                                      │
                                      ├─ Lee cursor (último id procesado)
                                      ├─ SELECT id FROM <tabla> WHERE id > cursor LIMIT 50
                                      ├─ Para cada fila → DeliverWebhookJob (cola async)
                                      └─ UPDATE cursor = MAX(id)

DeliverWebhookJob:
  POST a tu URL  →  Tu 2xx → succeeded
                 →  4xx (408/429) → failed_permanently
                 →  5xx / timeout / 408 / 429 → retry con backoff [1m, 5m, 30m, 2h, 12h]

Bootstrap del cursor: la primera vez que un partner se suscribe a un evento, el cursor se inicializa con MAX(id) actual de la tabla — no recibirás histórico. Solo lo que pase de ese minuto en adelante.

Registrar un webhook

curl -X POST https://api-pub.vendty.com/v1/webhooks \
  -H "Authorization: Bearer $TOKEN_CON_WEBHOOKS_MANAGE" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "url": "https://partner.example.com/vendty-webhook",
    "events": ["sale.created", "sale.voided", "table_order.created"],
    "description": "Producción principal"
  }'

Response 201:

{
  "data": {
    "id": "whk_a3b8c7d9e1f2",
    "url": "https://partner.example.com/vendty-webhook",
    "events": ["sale.created", "sale.voided", "table_order.created"],
    "status": "active",
    "secret": "7f3b9d8c2401...64-hex-chars-total...",
    "secret_warning": "Store this secret now — it will not be shown again.",
    "consecutive_failures": 0,
    "created_at": "2026-05-13T18:30:00Z"
  }
}

Guarda el secret AHORA. No se mostrará otra vez. Si lo pierdes, elimina el webhook y crea uno nuevo.

Validaciones:

  • url debe ser HTTPS y un host alcanzable público (no IPs privadas, no localhost).
  • events[] debe contener al menos un evento del catálogo.
  • Máx 20 webhooks por cliente.

Cómo se ve un delivery

POST https://partner.example.com/vendty-webhook
User-Agent: Vendty-Webhook/1.0
Content-Type: application/json
X-Vendty-Event: sale.created
X-Vendty-Delivery-Id: whd_a3b8c7d9e1f2
X-Vendty-Webhook-Id: whk_a3b8c7d9e1f2
X-Vendty-Signature: t=1715789432,v1=fc3a2b1e4d9f...64-hex...

{
  "id": "evt_d3c7a8...",
  "type": "sale.created",
  "occurred_at": "2026-05-13T18:30:32Z",
  "api_version": "v1",
  "api_client": "<tu_api_client_public_id>",
  "data": { ...el mismo shape que GET /v1/sales/{id}... }
}

El campo data siempre coincide con la representación pública del recurso correspondiente (/v1/sales/{id}, /v1/restaurant/tables/{id}/order, etc.) — para que tu código de webhook reuse el mismo parser que tu integración pull.

Verificar la firma — OBLIGATORIO

Sin verificar la firma, un atacante puede forge eventos y disparar lógica de tu sistema. Verifica siempre.

PHP

function verifyVendtySignature(string $rawBody, string $sigHeader, string $secret, int $tolerance = 300): bool {
    if (! preg_match('/^t=(\d+),v1=([a-f0-9]{64})$/', $sigHeader, $m)) {
        return false;
    }
    [$_, $t, $v1] = $m;
    if (abs(time() - (int) $t) > $tolerance) {
        return false; // replay protection
    }
    $expected = hash_hmac('sha256', $t.'.'.$rawBody, $secret);
    return hash_equals($expected, $v1);
}

// En tu endpoint:
$rawBody = file_get_contents('php://input');
$sig     = $_SERVER['HTTP_X_VENDTY_SIGNATURE'] ?? '';
if (! verifyVendtySignature($rawBody, $sig, $YOUR_WEBHOOK_SECRET)) {
    http_response_code(401);
    exit;
}

Node.js

const crypto = require('crypto');

function verifyVendtySignature(rawBody, sigHeader, secret, toleranceSec = 300) {
  const m = sigHeader.match(/^t=(\d+),v1=([a-f0-9]{64})$/);
  if (!m) return false;
  const [, t, v1] = m;
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

app.post('/vendty-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyVendtySignature(req.body, req.headers['x-vendty-signature'], YOUR_SECRET)) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body);
  res.status(200).end();
  // ...procesa async
});

Python

import hmac, hashlib, re, time

def verify(raw_body: bytes, sig_header: str, secret: str, tolerance: int = 300) -> bool:
    m = re.match(r"^t=(\d+),v1=([a-f0-9]{64})$", sig_header)
    if not m: return False
    t, v1 = m.group(1), m.group(2)
    if abs(time.time() - int(t)) > tolerance: return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

Comportamiento ante fallos

Tu response Nuestro comportamiento
2xx (200, 201, 204) succeeded. Resetea consecutive_failures a 0.
5xx, timeout, conn refused Retry hasta 5 veces con delay [1m, 5m, 30m, 2h, 12h]. Tras los 5 → failed_permanently.
408, 429 Transient. Mismo retry que 5xx.
Otro 4xx (400, 401, 403, …) failed_permanently SIN retry. Incrementa consecutive_failures.

Auto-pause: si consecutive_failures llega a 5, el webhook pasa a status="paused". Reactívalo con PATCH /v1/webhooks/{id} {"status":"active"} (también resetea el contador).

Total ventana de retry: ~14.5 h. Si tu servicio está caído más, considera reconciliar con GET /v1/sales?date_from=... cuando vuelva.

Idempotency en tu lado

Implementa el handler como idempotente. Usa X-Vendty-Delivery-Id:

$deliveryId = $_SERVER['HTTP_X_VENDTY_DELIVERY_ID'];
if ($redis->exists("processed:{$deliveryId}")) {
    http_response_code(200);
    exit; // ya procesado, responde 200 igual para que dejemos de reintentar
}
// ...procesar...
$redis->setex("processed:{$deliveryId}", 86400 * 7, '1'); // 7 días
http_response_code(200);

Probar tu integración antes de eventos reales

curl -X POST https://api-pub.vendty.com/v1/webhooks/$WHK_ID/test \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $(uuidgen)"

Encola UN delivery de webhook.test SOLO a ese webhook. No requiere que tu webhook esté suscrito a webhook.test. Confirma que recibes el POST + valida la firma + responde 200.

Auditar deliveries

# Las últimas 25 entregas
curl "https://api-pub.vendty.com/v1/webhooks/$WHK_ID/deliveries" -H "Authorization: Bearer $TOKEN"

# Solo las fallidas
curl "https://api-pub.vendty.com/v1/webhooks/$WHK_ID/deliveries?status=failed" -H "Authorization: Bearer $TOKEN"

# De cierta fecha en adelante
curl "https://api-pub.vendty.com/v1/webhooks/$WHK_ID/deliveries?after=2026-05-01T00:00:00Z" -H "Authorization: Bearer $TOKEN"

# Cursor pagination — sigue links.next hasta null
curl "https://api-pub.vendty.com/v1/webhooks/$WHK_ID/deliveries?cursor=$CURSOR_FROM_PREVIOUS_PAGE"

Buenas prácticas

  1. Verifica la firma SIEMPRE. Sin excepción.
  2. Responde rápido (< 1s). Procesa async después del 200. Tu timeout es de 10 s.
  3. Idempotency en tu handler. Usa X-Vendty-Delivery-Id para deduplicar.
  4. NO confíes en el orden. Dos eventos del mismo tipo pueden llegar fuera de orden. Usa data.updated_at o event.occurred_at para resolver.
  5. Monitorea consecutive_failures de tus webhooks. Si sube, hay algo mal en tu lado.
  6. Polling de respaldo opcional. Si tu negocio NO tolera perder eventos: cada 30 min haz GET /v1/sales?date_from=<last_seen> y reconcilia.
  7. No expongas el secret ni siquiera en logs.

Limitaciones actuales

  • Solo 3 eventos disparados por scanner (sale.created, sale.voided, table_order.created). Otros eventos del catálogo están definidos pero no se emiten en modo solo-lectura.
  • Latencia mínima ~60 s (ventana del scanner). No es realtime puro.
  • No hay retry manual desde la API.
  • No hay filtros avanzados de suscripción (todos los eventos del tipo se envían a todos los webhooks suscritos).
  • Rotación del secret = eliminar webhook + crear nuevo.

Sistema

Health check e identidad del cliente API

Productos

Catálogo de productos y categorías

Clientes

Terceros (clientes) y grupos

Listar clientes (terceros) del negocio

Catálogo de clientes con paginación. Requiere customers:read. El response NUNCA expone password, remember_token, numero_cuenta ni entidad_bancaria.

Authorizations:
bearerAuth
query Parameters
page
integer >= 1
Default: 1
per_page
integer [ 1 .. 200 ]
Default: 25
search
string <= 80 characters

Substring sobre razón social, nombre comercial y NIT (case-insensitive).

identification
string <= 30 characters

NIT/identificación exacta. Útil para deduplicación.

group_id
string^cg_[1-9A-HJ-NP-Za-km-z]{8,16}$
updated_since
string <date-time>
sort
string
Default: "-id"
Enum: "business_name" "-business_name" "identification" "-identification" "id" "-id"

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {},
  • "links": {}
}

Detalle de cliente por ID público

Authorizations:
bearerAuth
path Parameters
id
required
string^c_[1-9A-HJ-NP-Za-km-z]{8,16}$

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Historial de ventas de un cliente (paginación cursor)

Devuelve las ventas del cliente con paginación por cursor (escala a clientes con miles de ventas). Requiere AMBOS scopes customers:read y sales:read. Iterar siguiendo links.next hasta que sea null.

Authorizations:
bearerAuth
path Parameters
id
required
string^c_[1-9A-HJ-NP-Za-km-z]{8,16}$
query Parameters
cursor
string
per_page
integer [ 1 .. 100 ]
Default: 25

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {},
  • "links": {}
}

Catálogo de grupos de clientes

Tabla grupo_clientes (legacy, esquema mínimo: id, nombre).

Authorizations:
bearerAuth

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Ventas

Ventas, líneas de detalle, pagos y métodos de pago

Listar ventas del negocio (paginación cursor)

Paginación por cursor — diseñada para escalar a tenants con millones de ventas.

Por defecto solo retorna ventas válidas (status=valid). Pasa ?status=voided para ver anuladas o ?status=all para ambas. El rango date_fromdate_to no puede exceder 90 días.

Cómo iterar: primer request sin cursor, luego sigue links.next (o usa ?cursor=<meta.next_cursor>) hasta que sea null. per_page por defecto 25, máximo 100. NO se devuelve total ni last_page — ver schema CursorMeta.

Authorizations:
bearerAuth
query Parameters
cursor
string

Token de cursor opaco devuelto en meta.next_cursor del request previo. Omitir en el primer request.

per_page
integer [ 1 .. 100 ]
Default: 25
date_from
string <date>
date_to
string <date>
status
string
Default: "valid"
Enum: "valid" "voided" "all"
customer_id
string^c_[1-9A-HJ-NP-Za-km-z]{8,16}$
warehouse_id
string^wh_[1-9A-HJ-NP-Za-km-z]{8,16}$
seller_id
string^sl_[1-9A-HJ-NP-Za-km-z]{8,16}$
invoice_number
string <= 60 characters
include
string
Example: include=details,payments

Subconjunto de details, payments. Por defecto se eager-loadean customer, seller, warehouse.

sort
string
Default: "-date"
Enum: "date" "-date" "total" "-total" "id" "-id"

Orden. El servidor agrega id como tiebreaker para garantizar estabilidad del cursor.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {},
  • "links": {}
}

Listar notas crédito de una venta

Devuelve todas las NCs registradas para una venta (incluyendo las creadas desde el POS, desde devoluciones, o desde esta API). Paginación offset clásica — una venta rara vez tiene más de 1-2 NCs. Requiere scope credit-notes:read.

Authorizations:
bearerAuth
path Parameters
id
required
string^s_[1-9A-HJ-NP-Za-km-z]{8,16}$
query Parameters
page
integer >= 1
Default: 1
per_page
integer [ 1 .. 200 ]
Default: 25

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {},
  • "links": {}
}

Detalle completo de una venta

Eager-load automático de customer, seller, warehouse, details, payments (con sus métodos).

Authorizations:
bearerAuth
path Parameters
id
required
string^s_[1-9A-HJ-NP-Za-km-z]{8,16}$

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Catálogo de métodos de pago del negocio

Authorizations:
bearerAuth

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Restaurante

Pisos/áreas, mesas con estado y órdenes activas en mesas (solo tenants con opciones.tipo_negocio='restaurante')

Listar pisos/secciones del restaurante

Solo para tenants con opciones.tipo_negocio='restaurante'. Si el tenant no es restaurante, retorna 422 tenant_not_restaurant.

Excluye automáticamente la sección sintética id=-1 (Quick Service) y, por defecto, las inactivas.

Cada sección incluye tables_count (mesas activas asociadas) y occupied_tables_count (mesas con al menos un ítem en orden_producto_restaurant), calculados en una sola query.

Authorizations:
bearerAuth
query Parameters
warehouse_id
string^wh_[1-9A-HJ-NP-Za-km-z]{8,16}$
active
boolean
Default: true
page
integer >= 1
Default: 1
per_page
integer [ 1 .. 200 ]
Default: 25

Responses

Response samples

Content type
application/json
{}

Listar mesas del restaurante con estado computado

Solo para tenants restaurante. El campo status se computa en server (occupied si hay al menos un item en orden_producto_restaurant.mesa_id, sino free). kitchen_summary agrega por estado. vendedor_estacion=-1 se traduce a seller: null.

Authorizations:
bearerAuth
query Parameters
warehouse_id
string^wh_[1-9A-HJ-NP-Za-km-z]{8,16}$
section_id
string^rs_[1-9A-HJ-NP-Za-km-z]{8,16}$
status
string
Default: "all"
Enum: "occupied" "free" "all"
active
boolean
Default: true
page
integer >= 1
Default: 1
per_page
integer [ 1 .. 200 ]
Default: 25

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {},
  • "links": {}
}

Detalle de la orden activa en una mesa

Devuelve los items pre-venta actualmente en la mesa, con resolución completa de:

  • Producto principal (id, code, name).
  • Modificaciones (texto libre del legacy, devuelto como array de strings).
  • Adiciones (IDs de producto en legacy → resueltos a {product, quantity}; IDs duplicados se cuentan como cantidad).
  • Variations (IDs de producto_variation → JOIN con variation{id, name, code}).
  • Kitchen tracking desde comanda_virtual_productos (si el tenant tiene opciones.comanda_virtual='si').
Authorizations:
bearerAuth
path Parameters
id
required
string^rt_[1-9A-HJ-NP-Za-km-z]{8,16}$

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Quick Service

Órdenes de Quick Service (zona=-1), separadas del flujo restaurante

Listar órdenes de Quick Service (zona=-1)

Items con zona=-1, agrupados por su mesa_id virtual (timestamp Unix generado on-the-fly por systempos). Cada grupo representa un "ticket" de Quick Service. Ordenados por actividad reciente desc.

Authorizations:
bearerAuth
query Parameters
warehouse_id
string^wh_[1-9A-HJ-NP-Za-km-z]{8,16}$

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {
    }
}

Catálogos

Almacenes, impuestos, vendedores, usuarios y listas de precio del negocio

Listar almacenes/bodegas del negocio

Catálogo de almacenes. Scope catalogs:read. Por defecto retorna los activos (cuando la columna activo existe).

Authorizations:
bearerAuth
query Parameters
active
boolean
Default: true
page
integer >= 1
Default: 1
per_page
integer [ 1 .. 200 ]
Default: 25

Responses

Response samples

Content type
application/json
{}

Listar impuestos

Catálogo de impuestos. Scope catalogs:read. Cada impuesto incluye external_code (mapeo a códigos DIAN o sistemas externos del partner; puede ser null).

Authorizations:
bearerAuth
query Parameters
page
integer >= 1
Default: 1
per_page
integer [ 1 .. 200 ]
Default: 25

Responses

Response samples

Content type
application/json
{}

Listar vendedores/comisionistas

Catálogo de vendedores con sus datos (cédula, email, teléfono, comisión, almacén). Scope catalogs:read. Filtro ?warehouse_id opcional.

Authorizations:
bearerAuth
query Parameters
warehouse_id
string^wh_[1-9A-HJ-NP-Za-km-z]{8,16}$
page
integer >= 1
Default: 1
per_page
integer [ 1 .. 200 ]
Default: 25

Responses

Response samples

Content type
application/json
{}

Listar usuarios humanos del negocio

Catálogo de usuarios (cajeros, cocineros, admins) del negocio. Scope catalogs:read.

Aislamiento cross-tenant garantizado: el endpoint filtra WHERE db_config_id = <tu tenant> antes de retornar nada. Nunca puedes ver usuarios de otro tenant.

Campos sensibles BLOQUEADOS (jamás presentes en la respuesta): password, salt, remember_token, api_token, forgotten_password_code, activation_code, ip_address.

Authorizations:
bearerAuth
query Parameters
is_admin
boolean

Filtrar solo admins (true) o solo no-admins (false)

page
integer >= 1
Default: 1
per_page
integer [ 1 .. 200 ]
Default: 25

Responses

Response samples

Content type
application/json
{}

Listar listas de precio

Catálogo de listas de precio (lista_precios). Scope catalogs:read.

Solo el header de la lista (nombre, vigencia, almacén, grupo cliente). Las reglas detalladas por producto (specific_price_rule_*) son un caso aparte que se entregará en una fase futura si emerge demanda.

Authorizations:
bearerAuth
query Parameters
warehouse_id
string^wh_[1-9A-HJ-NP-Za-km-z]{8,16}$
page
integer >= 1
Default: 1
per_page
integer [ 1 .. 200 ]
Default: 25

Responses

Response samples

Content type
application/json
{}

Compras

Facturas de compra a proveedores, pagos y proveedores

Listar proveedores

Catálogo de proveedores. Scope purchases:read. Filtros: ?search, ?identification. NUNCA expone entidad_bancaria ni numero_cuenta.

Authorizations:
bearerAuth
query Parameters
search
string <= 80 characters
identification
string <= 30 characters
page
integer >= 1
Default: 1
per_page
integer [ 1 .. 200 ]
Default: 25

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {},
  • "links": {}
}

Listar facturas de compra

Listado de órdenes de compra a proveedores. Scope purchases:read. Cursor pagination porque el histórico crece (cafeteriavendty tiene 361 filas).

Filtros: ?status=valid|voided|all (default valid), ?date_from, ?date_to, ?warehouse_id, ?supplier_id.

Authorizations:
bearerAuth
query Parameters
cursor
string
per_page
integer [ 1 .. 100 ]
Default: 25
status
string
Default: "valid"
Enum: "valid" "voided" "all"
date_from
string <date>
date_to
string <date>
warehouse_id
string^wh_[1-9A-HJ-NP-Za-km-z]{8,16}$
supplier_id
string^sup_[1-9A-HJ-NP-Za-km-z]{8,16}$

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {},
  • "links": {}
}

Detalle de factura de compra

Eager-load de proveedor, almacén, líneas y pagos.

Authorizations:
bearerAuth
path Parameters
id
required
string^po_[1-9A-HJ-NP-Za-km-z]{8,16}$

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Documentos Soporte

Comprobantes a no-obligados a facturar (DIAN Colombia)

Listar documentos soporte

Listado de documentos soporte (DIAN: comprobantes a no-obligados). Scope purchases:read. Cursor pagination.

Authorizations:
bearerAuth
query Parameters
cursor
string
per_page
integer [ 1 .. 100 ]
Default: 25
date_from
string <date>
date_to
string <date>
warehouse_id
string^wh_[1-9A-HJ-NP-Za-km-z]{8,16}$
supplier_id
string^sup_[1-9A-HJ-NP-Za-km-z]{8,16}$

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {},
  • "links": {}
}

Detalle de documento soporte

Authorizations:
bearerAuth
path Parameters
id
required
string^supdoc_[1-9A-HJ-NP-Za-km-z]{8,16}$

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Reportes

Sistema async de reportes generados desde systempos (descarga via S3)

Listar reports async

Lista los jobs de reportes async creados desde systempos. Scope reports:read.

Tenant gating: si el tenant no está inscrito en async_reports_enabled_clients con enabled=1, retorna 422 tenant_not_async_enabled.

Cursor pagination — los jobs históricos crecen (cafeteriavendty tiene 911 jobs).

Filtros: ?status=pending|processing|completed|failed, ?report_type=stock_minimo_maximo|....

El campo parameters_summary está sanitizado — solo expone keys "safe" (almacen, fecha_inicio, fecha_fin, etc.). Internal keys (db_config_id, is_admin) se omiten.

Authorizations:
bearerAuth
query Parameters
cursor
string
per_page
integer [ 1 .. 100 ]
Default: 25
status
string
report_type
string

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {},
  • "links": {}
}

Detalle de un report job

Authorizations:
bearerAuth
path Parameters
id
required
string^rj_[1-9A-HJ-NP-Za-km-z]{8,16}$

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Webhooks

Suscripciones a eventos push y auditoría de entregas

Listar webhooks del cliente

Paginación offset clásica. Cross-ApiClient isolation — solo ves los webhooks que tu propio token creó.

Authorizations:
bearerAuth
query Parameters
page
integer >= 1
Default: 1
per_page
integer [ 1 .. 200 ]
Default: 25

Responses

Response samples

Content type
application/json
{}

Crear webhook

Crea una suscripción webhook. Requiere scope webhooks:manage y Idempotency-Key.

El secret se devuelve UNA SOLA VEZ en esta respuesta. Almacénalo de forma segura — todo GET posterior lo omite.

URL: HTTPS obligatorio en producción. HTTP solo para localhost, 127.0.0.1 y rangos RFC1918 (10.x, 172.16-31.x, 192.168.x) para desarrollo local del partner.

Eventos válidos: ver schema WebhookCreate.events para el catálogo actual.

Firma: cada delivery incluye X-Vendty-Signature: t=<unix>,v1=<hex_hmac>, donde hmac = HMAC-SHA256(secret, "<t>.<raw_body>"). Rechaza si now - t > 5 min.

Authorizations:
bearerAuth
header Parameters
Idempotency-Key
required
string <uuid>
Request Body schema: application/json
required
url
required
string <uri> <= 500 characters

Endpoint del partner. HTTPS obligatorio en producción. HTTP solo para localhost / RFC1918 (10.x, 172.16-31.x, 192.168.x).

events
required
Array of strings [ 1 .. 20 ] items
Items Enum: "sale.created" "sale.voided" "table_order.created" "credit_note.created" "customer.created" "customer.updated" "webhook.test"
description
string or null <= 200 characters

Responses

Request samples

Content type
application/json
{}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Detalle de webhook

Authorizations:
bearerAuth
path Parameters
id
required
string^whk_[a-f0-9]{32}$

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Actualizar webhook

Actualización parcial. Solo se pueden enviar url, events, description o status (activepaused). DELETE (soft-delete) se hace con el endpoint dedicado, NO con status: deleted (rechazado con 422). Cuando se reactiva un webhook pausado (status: active), el contador consecutive_failures se resetea a 0.

Authorizations:
bearerAuth
path Parameters
id
required
string^whk_[a-f0-9]{32}$
header Parameters
Idempotency-Key
required
string <uuid>
Request Body schema: application/json
required
url
string <uri> <= 500 characters
events
Array of strings [ 1 .. 20 ] items
Items Enum: "sale.created" "sale.voided" "table_order.created" "credit_note.created" "customer.created" "customer.updated" "webhook.test"
description
string or null <= 200 characters
status
string
Enum: "active" "paused"

Responses

Request samples

Content type
application/json
{
  • "events": [
    ],
  • "description": "string",
  • "status": "active"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Eliminar (soft delete)

Marca el webhook como deleted. Deliveries pendientes que aún estén en queue NO se envían.

Authorizations:
bearerAuth
path Parameters
id
required
string^whk_[a-f0-9]{32}$
header Parameters
Idempotency-Key
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "error": {
    }
}

Auditar entregas

Lista las entregas del webhook con paginación por cursor. Soporta filtros ?status= y ?event_type=. El status acepta el alias failed que combina failed_retryable + failed_permanently.

Authorizations:
bearerAuth
path Parameters
id
required
string^whk_[a-f0-9]{32}$
query Parameters
cursor
string
per_page
integer [ 1 .. 100 ]
Default: 25
status
string
Enum: "pending" "in_flight" "succeeded" "failed" "failed_retryable" "failed_permanently"
event_type
string
after
string <date-time>

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {},
  • "links": {}
}

Disparar evento sintético `webhook.test`

Útil para validar tu integración antes de empezar a recibir eventos reales. Dispara UN WebhookDelivery con event_type=webhook.test SOLO contra este webhook. No requiere que el webhook esté suscrito a webhook.test. Devuelve 409 si el webhook está pausado o eliminado.

Authorizations:
bearerAuth
path Parameters
id
required
string^whk_[a-f0-9]{32}$
header Parameters
Idempotency-Key
required
string <uuid>

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Health check

Responses

Response samples

Content type
application/json
{
  • "status": "ok"
}

Info del cliente API autenticado

Authorizations:
bearerAuth

Responses

Products

List products of the authenticated tenant

Returns the tenant's product catalog. Requires products:read scope. Default returns only active products; pass active=false for inactive. Use include=stock (with warehouse_id) to embed stock for one warehouse.

Authorizations:
bearerAuth
query Parameters
page
integer >= 1
Default: 1
per_page
integer [ 1 .. 200 ]
Default: 25
search
string <= 80 characters

Case-insensitive substring search across name and code.

category_id
string^cat_[1-9A-HJ-NP-Za-km-z]{8,16}$

Filter by category (opaque public ID).

warehouse_id
string^wh_[1-9A-HJ-NP-Za-km-z]{8,16}$

Required when include=stock. Filters stock data to one warehouse.

active
boolean
Default: true
include
string
Example: include=stock,category

Comma-separated. Supported tokens: stock (requires warehouse_id), category. category is eagerly loaded regardless to avoid N+1.

updated_since
string <date-time>

Returns products created/updated at or after this timestamp. Legacy schemas use producto.fecha fallback.

sort
string
Default: "-created_at"
Enum: "name" "-name" "price" "-price" "created_at" "-created_at"

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "meta": {},
  • "links": {}
}

Single product by opaque public ID

Returns a product the authenticated tenant owns. Returns 404 for IDs from other tenants — never 403 — to avoid cross-tenant existence disclosure.

Authorizations:
bearerAuth
path Parameters
id
required
string^p_[1-9A-HJ-NP-Za-km-z]{8,16}$

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

List tenant categories

Returns active categories. Requires products:read.

Authorizations:
bearerAuth

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}