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
POST /webhooks| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
url | string (URL) | Sí | Endpoint HTTPS de tu servidor que recibirá las notificaciones. |
events | array | Sí | Mí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.
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
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
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:
{
"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ódigo | HTTP | Causa |
|---|---|---|
validation_error | 400 | url inválida, events vacío o con un evento desconocido. |
unauthorized | 401 | API key ausente o inválida. |
Listar suscripciones
GET /webhooksEste endpoint no está paginado y devuelve todas tus suscripciones. El secret nunca se incluye en el listado.
curl "https://api-pos.zelta.dev/public/v1/webhooks" \
-H "Authorization: Bearer zpk_live_xxx"Ejemplo JS
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
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:
{
"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
PUT /webhooks/{id}| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
id | string (cuid2) | Sí | Identificador de la suscripción (en la ruta). |
url | string (URL) | No | Nuevo endpoint. |
events | array | No | Nueva lista de eventos (mínimo uno). |
isActive | boolean | No | Activa 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.
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
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
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:
{
"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
DELETE /webhooks/{id}| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
id | string (cuid2) | Sí | Identificador de la suscripción. |
curl -X DELETE "https://api-pos.zelta.dev/public/v1/webhooks/wh2k5m8p1q4v7r0n3t6w9y2z" \
-H "Authorization: Bearer zpk_live_xxx"Ejemplo JS
await fetch('https://api-pos.zelta.dev/public/v1/webhooks/wh2k5m8p1q4v7r0n3t6w9y2z', {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${process.env.ZELTA_POS_API_KEY}` }
});Ejemplo Py
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:
{
"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:
| Header | Descripción |
|---|---|
X-Zelta-Event | Nombre del evento (ej. sale.completed). |
X-Zelta-Signature | Firma 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.
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
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', 200TIP
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
| Evento | Descripción |
|---|---|
invoice.enrolled | Una factura fue enrolada (emitida fiscalmente). |
invoice.cancelled | Una factura fiscal fue anulada. |
Inventario
| Evento | Descripción |
|---|---|
stock.updated | El stock de una variante cambió. |
stock.low | El stock de una variante bajó del umbral mínimo. |
Catálogo
| Evento | Descripción |
|---|---|
product.created | Se creó un producto. |
product.updated | Se actualizó un producto. |
product.deleted | Se eliminó un producto. |
category.created | Se creó una categoría. |
category.updated | Se actualizó una categoría. |
category.deleted | Se eliminó una categoría. |
brand.created | Se creó una marca. |
brand.updated | Se actualizó una marca. |
brand.deleted | Se eliminó una marca. |
Ventas
| Evento | Descripción |
|---|---|
sale.completed | Una venta se completó. |
sale.delivered | Una venta se marcó como entregada. |
sale.cancelled | Una venta fue cancelada. |
sale.expired | Una venta (orden) expiró. |
payment.recorded | Se registró un pago. |
payment.cancelled | Se anuló un pago. |
return.completed | Se completó una devolución. |
Contactos
| Evento | Descripción |
|---|---|
contact.created | Se creó un contacto (cliente o proveedor). |
contact.updated | Se actualizó un contacto. |
contact.deleted | Se eliminó un contacto. |
Compras
| Evento | Descripción |
|---|---|
bill.created | Se registró una compra. |
bill.updated | Se actualizó una compra. |
bill.received | Una compra se marcó como recibida. |
bill.payment_recorded | Se registró un pago de una compra. |
bill.payment_cancelled | Se anuló un pago de una compra. |
bill.deleted | Se eliminó una compra. |
Gastos
| Evento | Descripción |
|---|---|
expense.created | Se registró un gasto. |
expense.updated | Se actualizó un gasto. |
expense.deleted | Se eliminó un gasto. |
Caja
| Evento | Descripción |
|---|---|
cash_session.closed | Se 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.