API publica v1

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.

1. Autenticacion

Toda request requiere el header Authorization: Bearer <secret>.

Dos tipos de keys:

  • Platform key (pk_live_…): emitida desde /operator/api-keys. Accede a TODOS los tenants. Uso interno de Zentra (zpanel.zentragroup.cl).
  • Tenant key (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..."

2. Scopes

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 /suppliers
  • platform:read — solo platform keys: /platform/*
  • * — todo lo anterior (default si no se especifica)

3. Rate limits

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: 1764100200

Al exceder, recibis 429 Too Many Requests con header Retry-After.

4. CORS / origenes

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).

5. Formato de errores

HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "error": {
    "code": "validation_error",
    "message": "Fecha invalida: foo"
  }
}

Status codes que puedes esperar:

  • 200 — exito
  • 400 — request invalido
  • 401 — API key faltante o invalida
  • 403 — scope insuficiente / kind incorrecto / origen no permitido / key revocada
  • 404 — recurso no existe (o no pertenece a tu tenant)
  • 429 — rate limit excedido
  • 5xx — error interno

6. Endpoint /me

Devuelve 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"
}

7. /reports/*

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/margin
  • by-branch, by-seller, by-day, by-category
  • top-products — top N por revenue (con margin por producto)

8. /sales (cursor pagination)

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 linea

9. /inventory/*

GET /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 umbral

10. /purchases/* + /suppliers

Compras a proveedores + gastos operativos. Cada registro tiene un kind: INVENTORY (mercaderia — suma stock) o EXPENSE (arriendo, luz, servicios, etc). Requiere scope purchases:read.

Listado paginado por cursor

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"
}

Resumen agregado del rango

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.

Detalle con items

GET /api/public/v1/purchases/<id>

# Devuelve el shape del listado + items[] (sku, cantidad, costo unitario,
# total linea) + notes, paymentMethod, paidAt.

Listar proveedores

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
  }
]

11. /platform/* (solo platform key)

Endpoints exclusivos para platform keys. Tenant keys reciben 403. Requieren scope platform:read salvo /platform/email/send que exige platform:email:send explicito.

Endpoints disponibles

  • GET /platform/tenants?siiStatus=&isActive=&plan= — lista con metricas
  • GET /platform/tenants/:tenantId — detalle completo
  • GET /platform/tenants/:tenantId/users — usuarios del tenant
  • GET /platform/tenants/:tenantId/billing-periods — historial de BillingPeriod
  • GET /platform/tenants/:tenantId/billing-history?year=&month= — snapshot mensual
  • GET /platform/tenants/:tenantId/export?section= — export para BI
  • GET /platform/revenue?from=&to= — revenue agregado del SaaS
  • GET /platform/plans — catalogo de planes
  • GET /platform/invoicing/summary?year=&month= — facturacion global del mes
  • POST /platform/email/send — envio transaccional (scope aparte)

Ejemplos

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..."
}

12. Webhooks

Configura una URL HTTPS en /admin/webhooks/new, elegi los eventos y recibi POSTs firmados. Eventos disponibles:

  • sale.created
  • sale.cancelled (opt-in)
  • factura.emitted (cuando el DTE pasa a ACCEPTED en el SII)
  • stock.low (dedupeado 1h por producto/sucursal)

Headers que recibis

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 */ }
}

Verificacion HMAC (Node.js)

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));
}

Verificacion HMAC (PHP)

<?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');
}

Reintentos

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.

13. Changelog

  • 2026-07-02 — v1.1.0 aditivo. Nueva seccion /purchases/* + /suppliers (compras y gastos operativos). Se documentan todos los endpoints /platform/* con ejemplos. Nuevos scopes purchases:read y platform:email:send.
  • 2026-05-23 — v1.0.0 release inicial. Endpoints read-only de reports, sales, inventory, platform. Webhooks para sale.created, sale.cancelled, factura.emitted, stock.low.