Read-only REST API para integrar Zentra POS con tus dashboards, e-commerce o sistemas externos. Versionada (/api/public/v1) con compromiso de 6 meses de notice ante cambios incompatibles.
¿Estas integrando por primera vez? Descarga la guia paso a paso (markdown) o el PDF con ejemplos completos en Node.js, PHP y Python.
Toda request requiere el header Authorization: Bearer <secret>.
Dos tipos de keys:
pk_live_…): emitida desde /operator/api-keys. Accede a TODOS los tenants. Uso interno de Zentra (zpanel.zentragroup.cl).tk_live_…): emitida desde /admin/api-keys. Acotada al tenant que la cree. La usa el cliente en su propio dashboard / web.El secret se muestra una sola vez al crear la key. Si lo perdes, hay que revocar y crear una nueva.
curl https://myzentrapos.app/api/public/v1/me \
-H "Authorization: Bearer tk_live_AB12CD3xY7zM..."Cada key tiene un array de scopes. "*" equivale a acceso total dentro de su kind. Si la key no tiene el scope requerido, el endpoint responde 403.
reports:read — endpoints /reports/*sales:read — endpoints /sales/*inventory:read — endpoints /inventory/*purchases:read — endpoints /purchases/* y /suppliersplatform:read — solo platform keys: /platform/** — todo lo anterior (default si no se especifica)Default 60 req/min por key (configurable al crear). Headers que devolvemos en cada response:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1764100200Al exceder, recibis 429 Too Many Requests con header Retry-After.
Tenant keys aceptan opcionalmente una allowlist de allowedOrigins. Si el header Origin esta presente y no matchea, respondemos 403. Si la allowlist esta vacia, requests browser-side son rechazadas (server-to-server only).
Platform keys son siempre server-to-server (no se exponen a browser).
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": {
"code": "validation_error",
"message": "Fecha invalida: foo"
}
}Status codes que puedes esperar:
200 — exito400 — request invalido401 — API key faltante o invalida403 — scope insuficiente / kind incorrecto / origen no permitido / key revocada404 — recurso no existe (o no pertenece a tu tenant)429 — rate limit excedido5xx — error internoDevuelve info de la key activa. Util como smoke test.
GET /api/public/v1/me
{
"kind": "TENANT",
"tenantId": "uuid",
"tenantSlug": "midemo",
"tenantName": "Mi Negocio SpA",
"scopes": ["*"],
"keyPrefix": "tk_live_AB12",
"name": "Dashboard cliente",
"expiresAt": "2027-05-23T20:15:00Z"
}Todos aceptan ?from=YYYY-MM-DD&to=YYYY-MM-DD&branchId=<uuid>.
GET /api/public/v1/reports/summary?from=2026-05-01&to=2026-05-31
{
"from": "2026-05-01T00:00:00Z",
"to": "2026-05-31T23:59:59Z",
"revenue": 1250000,
"taxAmount": 200000,
"discountAmount": 35000,
"netRevenue": 1015000,
"totalCost": 720000,
"profit": 530000,
"profitMargin": 42.4,
"salesCount": 87,
"cancelledCount": 3
}summary — agregado con profit/marginby-branch, by-seller, by-day, by-categorytop-products — top N por revenue (con margin por producto)GET /api/public/v1/sales?limit=50&from=2026-05-01
{
"items": [
{
"id": "uuid",
"status": "PAID",
"documentType": "BOLETA",
"folio": 1234,
"total": 15000,
"totalCost": 9000,
"profit": 6000,
...
}
],
"nextCursor": "uuid-de-la-ultima-venta"
}
GET /api/public/v1/sales?limit=50&cursor=<nextCursor> # siguiente pagina
GET /api/public/v1/sales/<id> # detalle con items + profit por lineaGET /api/public/v1/inventory/stock?branchId=<uuid>&alert=low
[
{
"productId": "uuid",
"branchId": "uuid",
"branchCode": "SUC01",
"sku": "PROD-001",
"name": "Cafe 1kg",
"quantity": 5,
"averageCost": 1200,
"stockValue": 6000,
"lowStockThreshold": 10,
"alert": "low"
}
]
GET /api/public/v1/inventory/low-stock # alerta de items bajo umbralCompras a proveedores + gastos operativos. Cada registro tiene un kind: INVENTORY (mercaderia — suma stock) o EXPENSE (arriendo, luz, servicios, etc). Requiere scope purchases:read.
GET /api/public/v1/purchases?limit=50&from=2026-06-01&kind=EXPENSE
# Query params opcionales:
# from, to YYYY-MM-DD
# kind INVENTORY | EXPENSE
# supplierId uuid
# branchId uuid
# paymentStatus UNPAID | PARTIAL | PAID
# limit default 50, max 200
# cursor uuid de la ultima fila (siguiente pagina)
{
"data": [
{
"id": "uuid",
"kind": "EXPENSE",
"documentType": "FACTURA",
"documentNumber": "F-102938",
"issueDate": "2026-06-15",
"subtotalNet": 84000,
"taxAmount": 15960,
"totalAmount": 99960,
"paymentStatus": "PAID",
"expenseCategory": "utilities",
"supplier": { "id": "uuid", "razonSocial": "CGE Distribucion SA", "rut": "76.411.321-7" },
"branch": { "id": "uuid", "code": "SUC01", "name": "Sucursal 1" },
"itemsCount": 1,
"hasAttachment": true
}
],
"nextCursor": "uuid-o-null"
}GET /api/public/v1/purchases/summary?from=2026-06-01&to=2026-06-30
{
"totals": {
"inventoryTotal": 1250000, # compras de mercaderia
"expenseTotal": 380000, # gastos operativos
"grandTotal": 1630000,
"invoiceCount": 42,
"unpaidCount": 3,
"unpaidTotal": 189000
},
"bySupplier": [ { "supplierId": "uuid", "razonSocial": "...", "rut": "...", "total": 450000, "count": 8 } ],
"byBranch": [ { "branchId": "uuid", "code": "SUC01", "name": "...", "total": 900000, "count": 30 } ],
"byExpenseCategory": [ { "category": "utilities", "total": 120000, "count": 4 } ],
"byMonth": [ { "month": "2026-06", "inventory": 1250000, "expense": 380000 } ]
}Categorias posibles en expenseCategory: utilities, rent, salary, marketing, maintenance, logistics, tax, other.
GET /api/public/v1/purchases/<id>
# Devuelve el shape del listado + items[] (sku, cantidad, costo unitario,
# total linea) + notes, paymentMethod, paidAt.GET /api/public/v1/suppliers
[
{
"id": "uuid",
"rut": "76.411.321-7",
"razonSocial": "CGE Distribucion SA",
"giro": "Distribucion electrica",
"comuna": "Providencia",
"ciudad": "Santiago",
"isActive": true
}
]Endpoints exclusivos para platform keys. Tenant keys reciben 403. Requieren scope platform:read salvo /platform/email/send que exige platform:email:send explicito.
GET /platform/tenants?siiStatus=&isActive=&plan= — lista con metricasGET /platform/tenants/:tenantId — detalle completoGET /platform/tenants/:tenantId/users — usuarios del tenantGET /platform/tenants/:tenantId/billing-periods — historial de BillingPeriodGET /platform/tenants/:tenantId/billing-history?year=&month= — snapshot mensualGET /platform/tenants/:tenantId/export?section= — export para BIGET /platform/revenue?from=&to= — revenue agregado del SaaSGET /platform/plans — catalogo de planesGET /platform/invoicing/summary?year=&month= — facturacion global del mesPOST /platform/email/send — envio transaccional (scope aparte)GET /api/public/v1/platform/tenants?siiStatus=PRODUCTION
[
{
"tenantId": "uuid",
"slug": "cliente-x",
"razonSocial": "Cliente X SpA",
"siiStatus": "PRODUCTION",
"plan": "starter",
"isActive": true,
"salesCount": 234,
"revenue": 4500000,
"profit": 1820000,
"lastSaleAt": "2026-05-23T18:42:00Z",
"createdAt": "2026-04-10T00:00:00Z"
}
]
GET /api/public/v1/platform/revenue?from=2026-05-01&to=2026-05-31
# { totalRevenue, totalProfit, activeTenants, totalTenants }
POST /api/public/v1/platform/email/send
Authorization: Bearer pk_live_...
Content-Type: application/json
{
"to": "cliente@ejemplo.cl",
"subject": "Recordatorio de pago",
"html": "<p>Hola...</p>",
"text": "Hola..."
}Configura una URL HTTPS en /admin/webhooks/new, elegi los eventos y recibi POSTs firmados. Eventos disponibles:
sale.createdsale.cancelled (opt-in)factura.emitted (cuando el DTE pasa a ACCEPTED en el SII)stock.low (dedupeado 1h por producto/sucursal)POST /tu/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Zentra-Webhook/1.0
X-Zentra-Event: sale.created
X-Zentra-Delivery-Id: 01HXXX...
X-Zentra-Timestamp: 1764100200
X-Zentra-Signature: sha256=<hex(hmac_sha256(secret, "{timestamp{'}'}.{body{'}'}"))>
{
"event": "sale.created",
"eventId": "evt_...",
"tenantId": "uuid",
"createdAt": "2026-05-23T20:15:00Z",
"data": { /* SaleResponseDto completo */ }
}import crypto from 'node:crypto';
function verify(req, secret) {
const timestamp = req.headers['x-zentra-timestamp'];
const sig = req.headers['x-zentra-signature'];
const body = JSON.stringify(req.body); // o req.rawBody
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + body)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}<?php
$timestamp = $_SERVER['HTTP_X_ZENTRA_TIMESTAMP'];
$sig = $_SERVER['HTTP_X_ZENTRA_SIGNATURE'];
$body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit('Invalid signature');
}Si tu endpoint no responde 2xx en 10s, reintentamos con backoff: 1m → 5m → 30m → 2h → 12h (6 intentos en total). A los 10 fallos consecutivos el endpoint se desactiva automaticamente — reactivalo manualmente desde la UI.
Tu endpoint puede deduplicar por X-Zentra-Delivery-Id o por eventId del payload.
/purchases/* + /suppliers (compras y gastos operativos). Se documentan todos los endpoints /platform/* con ejemplos. Nuevos scopes purchases:read y platform:email:send.