Skip to content

Webhooks

Los webhooks permiten que Zelta POS notifique a tu sistema en tiempo real cuando ocurre un evento —una venta completada, un cambio de stock, una compra recibida, etc.— sin que tengas que consultar la API de forma periódica.

La URL base de producción es https://api-pos.zelta.dev/public/v1 y todas las rutas se expresan relativas a ella. Todas las peticiones deben hacerse sobre HTTPS e incluir tu API key. Consulta .

Crear una suscripción

http
POST /webhooks
CampoTipoRequeridoDescripción
urlstring (URL)Endpoint HTTPS de tu servidor que recibirá las notificaciones.
eventsarrayMínimo un evento, tomado del .

WARNING

El campo secret se devuelve una sola vez en la respuesta de creación. Guárdalo de inmediato: lo necesitarás para validar la firma de cada entrega y no se puede recuperar más tarde.

bash
curl -X POST "https://api-pos.zelta.dev/public/v1/webhooks" \
  -H "Authorization: Bearer zpk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://tu-servidor.com/zelta/webhooks",
    "events": ["sale.completed", "stock.low", "bill.received"]
  }'

Ejemplo JS

javascript
const res = await fetch('https://api-pos.zelta.dev/public/v1/webhooks', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${process.env.ZELTA_POS_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    url: 'https://tu-servidor.com/zelta/webhooks',
    events: ['sale.completed', 'stock.low', 'bill.received']
  })
});
const data = await res.json();

Ejemplo Py

python
import requests
res = requests.post(
    'https://api-pos.zelta.dev/public/v1/webhooks',
    headers={'Authorization': 'Bearer zpk_live_xxx', 'Content-Type': 'application/json'},
    json={
        'url': 'https://tu-servidor.com/zelta/webhooks',
        'events': ['sale.completed', 'stock.low', 'bill.received']
    }
)
data = res.json()

Respuesta 201 Created:

json
{
  "webhook": {
    "id": "wh2k5m8p1q4v7r0n3t6w9y2z",
    "url": "https://tu-servidor.com/zelta/webhooks",
    "events": ["sale.completed", "stock.low", "bill.received"],
    "isActive": true,
    "createdAt": "2026-01-31T21:00:00.000Z",
    "updatedAt": "2026-01-31T21:00:00.000Z"
  },
  "secret": "whsec_9f3a1c8e2b7d4051a6e9c2f8b3d70e14"
}

Problemas comunes

CódigoHTTPCausa
validation_error400url inválida, events vacío o con un evento desconocido.
unauthorized401API key ausente o inválida.

Listar suscripciones

http
GET /webhooks

Este endpoint no está paginado y devuelve todas tus suscripciones. El secret nunca se incluye en el listado.

bash
curl "https://api-pos.zelta.dev/public/v1/webhooks" \
  -H "Authorization: Bearer zpk_live_xxx"

Ejemplo JS

javascript
const res = await fetch('https://api-pos.zelta.dev/public/v1/webhooks', {
  headers: { 'Authorization': `Bearer ${process.env.ZELTA_POS_API_KEY}` }
});
const data = await res.json();

Ejemplo Py

python
import requests
res = requests.get(
    'https://api-pos.zelta.dev/public/v1/webhooks',
    headers={'Authorization': 'Bearer zpk_live_xxx'}
)
data = res.json()

Respuesta 200 OK:

json
{
  "webhooks": [
    {
      "id": "wh2k5m8p1q4v7r0n3t6w9y2z",
      "url": "https://tu-servidor.com/zelta/webhooks",
      "events": ["sale.completed", "stock.low", "bill.received"],
      "isActive": true,
      "createdAt": "2026-01-31T21:00:00.000Z",
      "updatedAt": "2026-01-31T21:00:00.000Z"
    }
  ]
}

Actualizar una suscripción

http
PUT /webhooks/{id}
CampoTipoRequeridoDescripción
idstring (cuid2)Identificador de la suscripción (en la ruta).
urlstring (URL)NoNuevo endpoint.
eventsarrayNoNueva lista de eventos (mínimo uno).
isActivebooleanNoActiva o pausa la entrega de eventos.

WARNING

Debes enviar al menos uno de los campos del cuerpo. El secret no se reemite al actualizar; sigue siendo válido el que recibiste al crear la suscripción.

bash
curl -X PUT "https://api-pos.zelta.dev/public/v1/webhooks/wh2k5m8p1q4v7r0n3t6w9y2z" \
  -H "Authorization: Bearer zpk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "events": ["sale.completed", "sale.cancelled"], "isActive": false }'

Ejemplo JS

javascript
const res = await fetch('https://api-pos.zelta.dev/public/v1/webhooks/wh2k5m8p1q4v7r0n3t6w9y2z', {
  method: 'PUT',
  headers: { 'Authorization': `Bearer ${process.env.ZELTA_POS_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ events: ['sale.completed', 'sale.cancelled'], isActive: false })
});
const data = await res.json();

Ejemplo Py

python
import requests
res = requests.put(
    'https://api-pos.zelta.dev/public/v1/webhooks/wh2k5m8p1q4v7r0n3t6w9y2z',
    headers={'Authorization': 'Bearer zpk_live_xxx', 'Content-Type': 'application/json'},
    json={ 'events': ['sale.completed', 'sale.cancelled'], 'isActive': False }
)
data = res.json()

Respuesta 200 OK:

json
{
  "webhook": {
    "id": "wh2k5m8p1q4v7r0n3t6w9y2z",
    "url": "https://tu-servidor.com/zelta/webhooks",
    "events": ["sale.completed", "sale.cancelled"],
    "isActive": false,
    "createdAt": "2026-01-31T21:00:00.000Z",
    "updatedAt": "2026-02-01T09:30:00.000Z"
  }
}

Si la suscripción no existe, responde 404 not_found.

Eliminar una suscripción

http
DELETE /webhooks/{id}
CampoTipoRequeridoDescripción
idstring (cuid2)Identificador de la suscripción.
bash
curl -X DELETE "https://api-pos.zelta.dev/public/v1/webhooks/wh2k5m8p1q4v7r0n3t6w9y2z" \
  -H "Authorization: Bearer zpk_live_xxx"

Ejemplo JS

javascript
await fetch('https://api-pos.zelta.dev/public/v1/webhooks/wh2k5m8p1q4v7r0n3t6w9y2z', {
  method: 'DELETE',
  headers: { 'Authorization': `Bearer ${process.env.ZELTA_POS_API_KEY}` }
});

Ejemplo Py

python
import requests
requests.delete(
    'https://api-pos.zelta.dev/public/v1/webhooks/wh2k5m8p1q4v7r0n3t6w9y2z',
    headers={'Authorization': 'Bearer zpk_live_xxx'}
)

Responde 204 No Content (sin cuerpo) si la eliminación fue exitosa, o 404 not_found si la suscripción no existe.

Entrega de eventos

Cuando ocurre un evento al que estás suscrito, Zelta POS envía una petición POST a tu url con el siguiente cuerpo:

json
{
  "id": "ev2k5m8p1q4v7r0n3t6w9y2z",
  "event": "sale.completed",
  "createdAt": "2026-02-01T10:15:00.000Z",
  "data": {
    "id": "sl8h3k6m9p2q5v8r1n4t7w0y",
    "number": "VTA-001204",
    "total": 89.90
  }
}

Cada entrega incluye estos headers:

HeaderDescripción
X-Zelta-EventNombre del evento (ej. sale.completed).
X-Zelta-SignatureFirma en formato sha256=<HMAC-SHA256> del cuerpo crudo, calculada con tu secret.

Verificar la firma

Para confirmar que la notificación proviene de Zelta POS, calcula el HMAC-SHA256 del cuerpo crudo (los bytes exactos recibidos, sin re-serializar el JSON) usando tu secret y compáralo, en tiempo constante, con el valor del header X-Zelta-Signature.

javascript
import crypto from 'node:crypto';
import express from 'express';

const app = express();

// Importante: usa el cuerpo CRUDO, no el JSON ya parseado.
app.post('/zelta/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const secret = process.env.ZELTA_POS_WEBHOOK_SECRET;
  const signature = req.header('X-Zelta-Signature') || '';

  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(req.body) // req.body es un Buffer con el cuerpo crudo
    .digest('hex');

  const valid = signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

  if (!valid) return res.status(401).send('Firma inválida');

  const payload = JSON.parse(req.body.toString('utf8'));
  console.log('Evento recibido:', payload.event);
  res.status(200).send('ok');
});

Ejemplo Py

python
import hashlib
import hmac
import os
from flask import Flask, request, abort

app = Flask(__name__)

@app.post('/zelta/webhooks')
def handle_webhook():
    secret = os.environ['ZELTA_POS_WEBHOOK_SECRET'].encode('utf-8')
    signature = request.headers.get('X-Zelta-Signature', '')

    # Importante: usa el cuerpo CRUDO (request.get_data()), no request.json.
    raw_body = request.get_data()
    expected = 'sha256=' + hmac.new(secret, raw_body, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(signature, expected):
        abort(401, 'Firma inválida')

    payload = request.get_json()
    print('Evento recibido:', payload['event'])
    return 'ok', 200

TIP

Responde con un código 2xx lo antes posible. Si tu endpoint no responde o devuelve un error, Zelta POS reintentará la entrega. Procesa la lógica pesada de forma asíncrona para evitar timeouts.

Catálogo de eventos

Zelta POS ofrece 33 eventos agrupados por dominio. Suscríbete solo a los que necesites.

Fiscal

EventoDescripción
invoice.enrolledUna factura fue enrolada (emitida fiscalmente).
invoice.cancelledUna factura fiscal fue anulada.

Inventario

EventoDescripción
stock.updatedEl stock de una variante cambió.
stock.lowEl stock de una variante bajó del umbral mínimo.
EventoDescripción
product.createdSe creó un producto.
product.updatedSe actualizó un producto.
product.deletedSe eliminó un producto.
category.createdSe creó una categoría.
category.updatedSe actualizó una categoría.
category.deletedSe eliminó una categoría.
brand.createdSe creó una marca.
brand.updatedSe actualizó una marca.
brand.deletedSe eliminó una marca.

Ventas

EventoDescripción
sale.completedUna venta se completó.
sale.deliveredUna venta se marcó como entregada.
sale.cancelledUna venta fue cancelada.
sale.expiredUna venta (orden) expiró.
payment.recordedSe registró un pago.
payment.cancelledSe anuló un pago.
return.completedSe completó una devolución.

Contactos

EventoDescripción
contact.createdSe creó un contacto (cliente o proveedor).
contact.updatedSe actualizó un contacto.
contact.deletedSe eliminó un contacto.

Compras

EventoDescripción
bill.createdSe registró una compra.
bill.updatedSe actualizó una compra.
bill.receivedUna compra se marcó como recibida.
bill.payment_recordedSe registró un pago de una compra.
bill.payment_cancelledSe anuló un pago de una compra.
bill.deletedSe eliminó una compra.

Gastos

EventoDescripción
expense.createdSe registró un gasto.
expense.updatedSe actualizó un gasto.
expense.deletedSe eliminó un gasto.

Caja

EventoDescripción
cash_session.closedSe cerró una sesión de caja.

Siguientes pasos

  • — referencia de los eventos de ventas.
  • — referencia de los eventos de compras.
  • — referencia de los eventos de stock.
  • — códigos de error y convenciones de la API.

Documentación oficial de Zelta