- Descripción General
- Endpoints
- Protocolo WebSocket
- Mensajes del Cliente → Servidor
- Mensajes del Servidor → Cliente
- HTTP Endpoints
- Autenticación
- Seguridad y Límites
- Categorías de Error
- Ejemplos de Integración
- Versionado
Ticket Daemon expone un servidor WebSocket para recibir trabajos de impresión desde aplicaciones POS y otros clientes. El protocolo es bidireccional y basado en mensajes JSON.
| Protocolo | Endpoint | Descripción |
|---|---|---|
| WebSocket | ws://{host}:8766/ws |
Conexión principal para trabajos |
| HTTP GET | http://{host}:8766/health |
Health check (monitoreo) |
| HTTP GET | http://{host}:8766/ |
Dashboard de diagnóstico (HTML) |
| Ambiente | Host Bind | Puerto | Servicio Windows |
|---|---|---|---|
| Producción | 0.0.0.0 |
8766 | TicketServicio |
| Test/Dev | localhost |
8766 | TicketServicioTest |
┌─────────────────────────────────────────────────────────────────┐
│ 1. Cliente conecta a ws://host:8766/ws │
│ 2. Servidor envía mensaje "info" de bienvenida │
│ 3. Cliente envía mensajes (ticket, status, ping) │
│ 4. Servidor responde (ack, result, error, pong, status) │
│ 5. Conexión se mantiene abierta hasta cierre explícito │
└─────────────────────────────────────────────────────────────────┘
Cliente Servidor Impresora
│ │ │
│──── ticket (job-001) ─────────>│ │
│ │── Validar JSON │
│ │── Encolar trabajo │
│<─── ack (queued, pos: 1) ──────│ │
│ │ │
│ │── Worker toma trabajo │
│ │───────── ESC/POS ─────────────>│
│ │<──────── OK ───────────────────│
│<─── result (success) ──────────│ │
│ │ │
Encola un documento para impresión. Requiere autenticación si el servidor fue compilado con AuthToken.
Estructura:
{
"tipo": "ticket",
"id": "pos1-20260115-001",
"auth_token": "tu-token-secreto",
"datos": {
"version": "1.0",
"profile": { "model": "58mm PT-210", "paper_width": 58 },
"commands": [
"..."
]
}
}
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
tipo |
string | ✓ | Debe ser "ticket" |
id |
string | ID del trabajo (si se omite, el servidor genera UUID) | |
auth_token |
string | ✓* | Requerido si el servidor tiene AuthToken configurado. |
datos |
object | ✓ | Documento de impresión (ver document.schema.json) |
Respuestas Posibles:
| Escenario | Respuesta |
|---|---|
| Trabajo encolado | ack con status "queued" |
Campo datos faltante |
error inmediato |
| Cola llena | error con mensaje de retry |
| Impresión exitosa | result con status success |
| Impresión fallida | result con status error |
Solicita estadísticas actuales de la cola de impresión.
Estructura:
{
"tipo": "status"
}| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
tipo |
string | ✓ | Debe ser "status" |
Envía un ping para verificar que el servidor responde.
Estructura:
{
"tipo": "ping",
"id": "ping-1736956800000"
}| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
tipo |
string | ✓ | Debe ser "ping" |
id |
string | ID opcional para correlacionar el pong |
Solicita la lista de impresoras instaladas en el sistema.
Estructura:
{
"tipo": "get_printers"
}| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
tipo |
string | ✓ | Debe ser "get_printers" |
Enviado al conectar (bienvenida) o para notificaciones generales.
{
"tipo": "info",
"status": "connected",
"mensaje": "✅ Servidor respondiendo desde Ticket Daemon"
}| Campo | Tipo | Descripción |
|---|---|---|
tipo |
string | Siempre "info" |
status |
string | Estado de conexión |
mensaje |
string | Mensaje legible para humanos |
Indica que el trabajo fue aceptado y está en cola.
{
"tipo": "ack",
"id": "pos1-20260115-001",
"status": "queued",
"current": 3,
"capacity": 100,
"mensaje": "Job queued for printing"
}| Campo | Tipo | Descripción |
|---|---|---|
tipo |
string | Siempre "ack" |
id |
string | ID del trabajo |
status |
string | Siempre "queued" |
current |
integer | Trabajos actualmente en cola |
capacity |
integer | Capacidad máxima de la cola |
mensaje |
string | Confirmación legible |
⚠️ Nota:acksolo confirma encolamiento en servicio, NO impresión. Espera el mensajeresultpara confirmar que el Windows Spooler registró el trabajo.
Enviado cuando un trabajo termina de procesarse (éxito o error).
Éxito:
{
"tipo": "result",
"id": "pos1-20260115-001",
"status": "success",
"mensaje": "Print completed in 245ms"
}Error:
{
"tipo": "result",
"id": "pos1-20260115-001",
"status": "error",
"mensaje": "PRINTER: Cannot connect - check if printer is installed"
}| Campo | Tipo | Descripción |
|---|---|---|
tipo |
string | Siempre "result" |
id |
string | ID del trabajo procesado |
status |
string | "success" o "error" |
mensaje |
string | Descripción del resultado o detalle de error |
Enviado cuando hay un error de validación o la cola está llena (antes de encolar).
{
"tipo": "error",
"id": "job-123",
"status": "error",
"mensaje": "Queue full, please retry in a few seconds"
}| Campo | Tipo | Descripción |
|---|---|---|
tipo |
string | Siempre "error" |
id |
string | ID del trabajo (si aplica) |
status |
string | Siempre "error" |
mensaje |
string | Descripción del error |
Errores Comunes:
| Mensaje | Causa |
|---|---|
Field 'datos' is required for type 'ticket' |
Falta el campo datos |
Queue full, please retry in a few seconds |
Cola saturada (100 trabajos) |
Rate limit exceeded: please wait before submitting more jobs |
Límite de 30 trabajos/min excedido |
Unknown message type: xxx |
Tipo de mensaje no reconocido |
{
"tipo": "pong",
"id": "ping-1736956800000",
"status": "ok"
}| Campo | Tipo | Descripción |
|---|---|---|
tipo |
string | Siempre "pong" |
id |
string | ID del ping original (si se envió) |
status |
string | Siempre "ok" |
{
"tipo": "status",
"status": "ok",
"current": 5,
"capacity": 100,
"mensaje": "Queue: 5/100"
}| Campo | Tipo | Descripción |
|---|---|---|
tipo |
string | Siempre "status" |
status |
string | Siempre "ok" |
current |
integer | Trabajos en cola |
capacity |
integer | Capacidad máxima |
mensaje |
string | Estado formateado |
Respuesta con información detallada de las impresoras instaladas.
{
"tipo": "printers",
"status": "ok",
"printers": [
{
"name": "58mm PT-210",
"port": "USB001",
"driver": "Generic / Text Only",
"status": "ready",
"is_default": true,
"is_virtual": false,
"printer_type": "thermal"
},
{
"name": "Microsoft Print to PDF",
"port": "PORTPROMPT:",
"driver": "Microsoft Print To PDF",
"status": "ready",
"is_default": false,
"is_virtual": true,
"printer_type": "virtual"
}
],
"summary": {
"status": "ok",
"detected_count": 2,
"thermal_count": 1,
"default_name": "58mm PT-210"
}
}| Campo | Tipo | Descripción |
|---|---|---|
tipo |
string | Siempre "printers" |
status |
string | Siempre "ok" |
printers |
array | Lista de objetos PrinterDetail |
summary |
object | Resumen del subsistema de impresoras |
PrinterDetail:
| Campo | Tipo | Descripción |
|---|---|---|
name |
string | Nombre de la cola de impresión |
port |
string | Puerto (USB001, LPT1, etc.) |
driver |
string | Nombre del driver |
status |
string | Estado: ready, offline, paused, error, unknown |
is_default |
boolean | Si es la impresora predeterminada |
is_virtual |
boolean | Si es virtual (PDF, XPS, etc.) |
printer_type |
string | Tipo: thermal, virtual, network, unknown |
PrinterSummary:
| Campo | Tipo | Descripción |
|---|---|---|
status |
string | ok (térmica detectada), warning (solo físicas), error (ninguna) |
detected_count |
integer | Total de impresoras instaladas |
thermal_count |
integer | Impresoras térmicas/POS detectadas |
default_name |
string | Nombre de la impresora predeterminada (si existe) |
Nota técnica: El campo
statusde cada impresora refleja el último estado reportado por el Windows Spooler. Para impresoras USB que no usan bidireccional, el estado puede no actualizarse hasta que se envíe un trabajo.
Endpoint de health check para sistemas de monitoreo.
Request:
GET /health HTTP/1.1
Host: localhost:8766Response:
{
"status": "ok",
"queue": {
"current": 2,
"capacity": 100,
"utilization": 4.0
},
"worker": {
"running": true,
"jobs_processed": 1547,
"jobs_failed": 3
},
"printers": {
"status": "ok",
"detected_count": 2,
"thermal_count": 1,
"default_name": "58mm PT-210"
},
"build": {
"env": "local",
"date": "unknown",
"time": "unknown"
},
"uptime": 86400
}Headers de Respuesta:
Content-Type: application/json
Access-Control-Allow-Origin: *
El servidor puede configurarse con un AUTH_TOKEN. Si está activo:
- El Dashboard (
/) requerirá inicio de sesión. - Los mensajes WebSocket de tipo
ticketdeben incluir el campo"auth_token"en la raíz del JSON.
Respuesta de Error de Autenticación:
Para enviar trabajos de impresión (type: "ticket"), es obligatorio incluir el campo "auth_token" coincidente con la
configuración del servidor. Si el token es incorrecto o falta, el servidor responderá con un error inmediato y no
encolará el trabajo.
{
"tipo": "error",
"id": "job-123",
"status": "error",
"mensaje": "Invalid or missing auth token"
}
El servidor protege la cola de impresión limitando la velocidad de peticiones:
- Límite: 30 trabajos por minuto por conexión WebSocket.
- Respuesta: Si se excede, se recibe un mensaje de tipo
errorcon el texto exacto:"Rate limit exceeded: please wait before submitting more jobs".
| Parámetro | Valor | Descripción |
|---|---|---|
| Capacidad de cola | 100 | Trabajos máximos en espera (por defecto) |
| Rate Limit | 30/min | Trabajos por minuto por conexión cliente |
| Timeout de notificación | 5s | Tiempo máximo para notificar resultado |
| Reconexión recomendada | 3s | Delay sugerido antes de reconectar |
Los mensajes de error en logs y en las respuestas WebSocket de tipo result con status: "error" siguen un formato
prefijado.
💡 Nota para integración: Los clientes pueden hacer un
split(":")del mensaje de error para separar la categoría técnica (ej.VALIDATION,PRINTER) de la descripción legible para el usuario.
| Prefijo | Descripción | Ejemplos |
|---|---|---|
VALIDATION: |
Error de estructura del documento | Missing 'version' field |
PRINTER: |
Error de conexión con impresora | Cannot connect - check if printer is installed |
QR: |
Error generando código QR | Data cannot be empty |
BARCODE: |
Error en código de barras | Symbology type is required |
TABLE: |
Error en renderizado de tabla | Columns were not defined |
RAW: |
Error en comando raw | Blocked by safe_mode |
IMAGE: |
Error procesando imagen | Invalid or corrupted base64 data |
COMMAND: |
Error de comando desconocido | Unknown command type |
JSON: |
Error de parsing JSON | Invalid document structure |
AUDIT: |
Error de seguridad | Invalid or missing auth token |
EXECUTION: |
Error durante ejecución | (varios) |
ERROR: |
Error genérico | (fallback) |
/**
* Cliente Mínimo para Ticket Daemon
* Uso:
* const client = new TicketClient({
* host: 'localhost',
* onSuccess: (id, msg) => console.log('Éxito:', id),
* onError: (err) => console.error('Error:', err)
* });
* client.connect();
*/
class TicketClient {
constructor(config = {}) {
this.wsUrl = `ws://${config.host || 'localhost'}:8766/ws`;
this.socket = null;
this.callbacks = {
onConnect: config.onConnect || (() => console.log('🔌 Conectado a Ticket Daemon')),
onDisconnect: config.onDisconnect || (() => console.log('❌ Desconectado')),
onSuccess: config.onSuccess || ((id, msg) => console.log(`✅ Impreso [${id}]: ${msg}`)),
onError: config.onError || ((msg) => console.error(`⚠️ Error: ${msg}`)),
onPrinters: config.onPrinters || ((list) => console.table(list))
};
}
connect() {
this.socket = new WebSocket(this.wsUrl);
this.socket.onopen = () => this.callbacks.onConnect();
this.socket.onclose = () => {
this.callbacks.onDisconnect();
// Reintento automático cada 3 segundos
setTimeout(() => this.connect(), 3000);
};
this.socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
this.handleMessage(msg);
};
}
handleMessage(msg) {
switch (msg.tipo) {
case 'result': // Resultado final de la impresión
if (msg.status === 'success') {
this.callbacks.onSuccess(msg.id, msg.mensaje);
} else {
this.callbacks.onError(`Fallo en trabajo ${msg.id}: ${msg.mensaje}`);
}
break;
case 'error': // Error inmediato (validación/cola)
this.callbacks.onError(msg.mensaje);
break;
case 'printers': // Respuesta de lista de impresoras
this.callbacks.onPrinters(msg.printers || []);
break;
case 'ack':
console.log(`📥 Encolado: ${msg.id} (Posición ${msg.current})`);
break;
}
}
/**
* Envía un documento a imprimir
* @param {Object} document - Objeto JSON con estructura de Poster
* @param {string} [id] - ID opcional del trabajo
* @param {string} [auth_token] - Token de autenticación si el servidor lo requiere
*/
print(document, id = null, authToken = null) {
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
this.callbacks.onError("No hay conexión con el servicio de impresión");
return;
}
const payload = {
tipo: 'ticket',
id: id || `job-${Date.now()}`,
datos: document
};
if (authToken) {
payload.auth_token = authToken;
}
this.socket.send(JSON.stringify(payload));
}
/**
* Solicita la lista de impresoras instaladas
*/
getPrinters() {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({tipo: 'get_printers'}));
}
}
}
// EJEMPLO DE USO RÁPIDO:
// ----------------------------------------
// const printerService = new TicketClient();
// printerService.connect();
//
// // Para imprimir:
// printerService.print({
// version: "1.0",
// profile: { model: "58mm PT-210" },
// commands: [{ type: "text", data: { content: { text: "Hola Mundo" } } }]
// });
//
// // Para obtener impresoras:
// printerService.getPrinters();package main
import (
"context"
"encoding/json"
"log"
"github.com/coder/websocket"
"github.com/coder/websocket/wsjson"
)
type Message struct {
Tipo string `json:"tipo"`
ID string `json:"id,omitempty"`
Datos json.RawMessage `json:"datos,omitempty"`
AuthToken string `json:"auth_token,omitempty"`
}
type Response struct {
Tipo string `json:"tipo"`
ID string `json:"id,omitempty"`
Status string `json:"status,omitempty"`
Mensaje string `json:"mensaje,omitempty"`
}
func main() {
ctx := context.Background()
conn, _, err := websocket.Dial(ctx, "ws://localhost:8766/ws", nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close(websocket.StatusNormalClosure, "")
// Enviar trabajo
doc := `{"version":"1.0","profile":{"model":"58mm PT-210"},"commands":[{"type":"beep","data":{"times":1}}]}`
msg := Message{
Tipo: "ticket",
ID: "go-job-001",
AuthToken: "tu-token-secreto", // Requerido si el servidor compiló con AuthToken
Datos: json.RawMessage(doc),
}
if err := wsjson.Write(ctx, conn, msg); err != nil {
log.Fatal(err)
}
// Leer respuestas
for {
var resp Response
if err := wsjson.Read(ctx, conn, &resp); err != nil {
break
}
log.Printf("[%s] %s: %s", resp.Tipo, resp.ID, resp.Mensaje)
if resp.Tipo == "result" {
break
}
}
}curl -s http://localhost:8766/health | jq .- Protocolo actual: v1.0
- Documento de impresión: v1.0 (ver
document.schema.json) - Compatibilidad: El servidor valida
versionen el documento pero actualmente solo soporta1.x