Referencia API
Las reglas comunes a todos los endpoints de la API de Zenci: URL base, formato de los datos, paginación, límites y errores.
URL base
https://api.zenci.app/public/v1Todas las rutas de esta documentación son relativas a esa base y todas las peticiones van por HTTPS. Cada petición lleva una clave de API; consulta .
Endpoints
| Método | Ruta | Permiso de la clave | Para qué sirve |
|---|---|---|---|
GET | /me | Ninguno | Ver qué permisos, sucursal y lista de precios tiene la clave |
GET | /products | catalog:read | Listar el catálogo, con sincronización incremental |
GET | /products/{id} | catalog:read | Leer un producto |
GET | /stock | stock:read | Consultar existencias por variante y bodega |
GET | /categories | catalog:read | Listar las categorías |
GET | /brands | catalog:read | Listar las marcas |
GET | /branches | orders:write o stock:read | Listar las sucursales y la bodega de cada una |
GET | /warehouses | orders:write o stock:read | Listar las bodegas |
GET | /payment-methods | orders:write | Listar las formas de pago con las que se puede cobrar |
POST | /orders | orders:write | Registrar una venta |
GET | /orders | orders:read | Listar las ventas que registró la clave |
GET | /orders/{id} | orders:read | Leer una venta con su factura |
GET | /invoices/{id}/pdf | Ninguno: enlace firmado | Descargar el PDF de una factura con el pdfUrl de la venta |
GET | /customers | customers:read | Buscar clientes |
GET | /customers/{id} | customers:read | Leer un cliente |
POST | /customers | customers:write | Crear un cliente |
PATCH | /customers/{id} | customers:write | Corregir un cliente |
POST | /customers/{id}/companies | customers:write | Agregar una compañía a la que se le factura |
DELETE | /customers/{id}/companies/{companyId} | customers:write | Quitar una compañía adicional |
No hay más endpoints que estos. Los avisos (webhooks) no se configuran por la API: se crean en el panel. Consulta .
Una ruta que no existe responde 404 con el código not_found.
Especificación OpenAPI
La descripción completa de la API, en OpenAPI 3.1, está en:
https://api.zenci.app/public/v1/openapi.jsonNo pide clave. Trae todos los endpoints, sus parámetros, sus respuestas y los avisos (en la sección webhooks). Puedes importarla en Postman o Insomnia, o generar un cliente para tu lenguaje.
Formato de los datos
- Cuerpo y respuestas en JSON. En
POST /orders, envía el cuerpo en JSON conContent-Type: application/json. - Dinero como texto con dos decimales:
"36.00","2.52". No envíes ni esperes números de JSON en los importes. - Cantidades como texto, sin ceros de más:
"1","12","0.5". - Tasa de impuesto como texto con dos decimales:
"7.00", onullsi el producto no lleva impuesto. - Fechas en ISO 8601, en UTC:
"2026-10-06T15:56:12.000Z". - Los precios no incluyen el impuesto. El impuesto se suma al registrar la venta; consulta .
Recurso único
GET /products/{id}, GET /me, GET /orders/{id}, GET /customers/{id} y las altas y cambios (POST, PATCH) devuelven el objeto tal cual, sin envoltorio. DELETE responde 204 sin cuerpo.
Listas
GET /products, GET /stock, GET /orders y GET /customers envuelven los resultados en data y agregan pagination:
{
"data": [ { "id": "k3v9x2m8q1w7e4r6t0y5u2i8" } ],
"pagination": {
"page": 1,
"limit": 50,
"total": 128,
"totalPages": 3
}
}GET /payment-methods, GET /categories, GET /brands, GET /branches y GET /warehouses devuelven { "data": [ … ] }, sin pagination: las listas son cortas y salen completas.
Paginación
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
page | entero ≥ 1 | 1 | Página que quieres leer |
limit | entero de 1 a 100 | 50 | Resultados por página |
Un limit mayor que 100 se rechaza con 400; no se recorta en silencio. Para leer todo, avanza page hasta llegar a totalPages. GET /products además devuelve nextCursor, que es la forma segura de recorrerlo para sincronizar; consulta .
Parámetros desconocidos
La API rechaza cualquier parámetro o campo que no conozca, en la URL y en el cuerpo. Un ?limt=20 mal escrito o un campo discount en una línea de venta devuelven 400 validation_error, en vez de ignorarse y hacerte creer que se aplicaron.
{
"code": "validation_error",
"message": "Unrecognized key: \"limt\""
}Límites de uso
| Límite | Valor | Qué pasa al superarlo |
|---|---|---|
| Peticiones por clave | 600 por minuto | 429 rate_limited |
| Peticiones sin clave, por IP | 200 por minuto | 429 rate_limited |
| Intentos con una clave inválida, por IP | 10 por minuto | 429 rate_limited y la IP queda bloqueada 60 segundos |
| Tamaño del cuerpo | 10 MB | 413 payload_too_large |
El cupo de 600 es de cada clave, no de tu servidor: dos tiendas que comparten un mismo hosting no se gastan el cupo entre sí.
Cada respuesta autenticada trae el límite en estos encabezados:
| Encabezado | Valor |
|---|---|
RateLimit-Policy | 600;w=60: 600 peticiones por ventana de 60 segundos |
X-RateLimit-Limit | 600 |
Retry-After | Solo en el 429: los segundos que conviene esperar antes de reintentar |
La API no informa cuántas peticiones te quedan en la ventana: el límite lo aplica la red de Cloudflare y no lo expone. Regula tu ritmo con la política y, si recibes un 429, espera lo que dice Retry-After.
Errores
Todo error responde con un estado HTTP y un cuerpo plano con dos campos:
{
"code": "not_found",
"message": "Product not found"
}| HTTP | code | Cuándo ocurre |
|---|---|---|
400 | validation_error | Parámetro o campo inválido, campo desconocido, cuerpo que no es JSON, falta X-Idempotency-Key, SKU que no existe, pago por encima del saldo con una forma de pago sin vuelto |
401 | unauthorized | Falta la clave, o es desconocida, está revocada o venció |
403 | forbidden | La clave no tiene el permiso que pide la ruta, o la suscripción del comercio está restringida |
404 | not_found | El recurso no existe, la ruta no existe, o el comercio no tiene activa la API |
409 | conflict | Los pagos no cubren el total de la venta, el reenvío de una venta que todavía se registra (con Retry-After) o que no terminó bien, o un cliente que cambió desde que lo leíste (expectedUpdatedAt) |
413 | payload_too_large | El cuerpo pasa de 10 MB |
429 | rate_limited | Superaste un límite de uso |
500 | internal_error | Falla inesperada del servidor |
Decide qué hacer con el estado HTTP y el code. El message explica el motivo en inglés y sirve para tus registros, pero su texto puede cambiar: no lo compares en tu código.
Un 500 nunca trae detalle ("An unexpected error occurred"). Si te ocurre en POST /orders, reintenta con la misma X-Idempotency-Key; consulta .
Siguientes pasos
- — Emite tu clave y envíala en cada petición.
- — Lee el catálogo y sincronízalo.
- — Registra una venta paso a paso.