Apariencia
Webhook de Cambio de Estado
Endpoint
PUT {tu_endpoint_configurado}Descripción
Webhook que TSALVA invoca automáticamente hacia tu sistema cada vez que se produce un cambio de estado en un servicio. Es fundamental para mantener sincronizada la información entre ambos sistemas.
Estados típicos notificados:
- Aceptación del servicio por central
- Técnico en camino
- Técnico en sitio
- Servicio en proceso
- Servicio completado
- Servicio cancelado/incompleto
Códigos de Estado
Los estados se envían como códigos numéricos en el campo codigoEstado:
| Código | Estado | Descripción |
|---|---|---|
0 | Ofertado | El servicio fue creado y distribuido a centrales |
1 | Recurso asignado | Una central asignó un técnico al servicio |
2 | Aceptado | La central aceptó el servicio oficialmente |
3 | En desplazamiento a origen | El técnico se dirige al punto de origen |
4 | En sitio | El técnico llegó al lugar del servicio |
5 | En desplazamiento al destino | El técnico se dirige al destino (si aplica) |
6 | Incompleto | El servicio no pudo completarse |
7 | Cancelado | El servicio fue cancelado |
8 | Finalizado | El servicio se completó exitosamente |
Configuración Previa
Requisitos:
- URL del endpoint: Debe ser accesible públicamente
- API Key: Clave para validar autenticidad
- Certificado SSL: HTTPS recomendado
Consideraciones técnicas:
- Timeout: 30 segundos máximo
- Reintentos: Hasta 3 veces en caso de error
- Respuesta esperada: Código 200 para confirmar recepción
Headers de la Petición
Content-Type: application/json
x-apikey: {tu_api_key_configurada}Validación de API Key
javascript
app.put('/webhook/cambio-estado', (req, res) => {
const apiKey = req.headers['x-apikey'];
if (apiKey !== process.env.TSALVA_API_KEY) {
return res.status(401).json({ error: 'API Key inválida' });
}
// Procesar webhook...
});Estructura del Payload
Campos principales
| Campo | Tipo | Descripción |
|---|---|---|
idServicio | string | Identificador único del servicio |
codigoEstado | number | Código numérico del nuevo estado (ver tabla de códigos) |
dniGestor | string | Documento del gestor que registra el cambio |
detalleEstado | object | Información detallada del estado actual |
Detalle del Estado
| Campo | Tipo | Descripción |
|---|---|---|
nitPrestador | string | NIT del prestador asignado |
nombrePrestador | string | Razón social del prestador |
telefonoCentral | string | Teléfono de contacto de la central |
latitud | string | Ubicación actual del técnico (latitud) |
longitud | string | Ubicación actual del técnico (longitud) |
idTecnico | string | Identificador único del técnico |
telefonoTecnico | string | Teléfono directo del técnico |
tarifaEstimada | string | Tarifa estimada para el servicio |
eta | string | Tiempo estimado de llegada (minutos) |
observaciones | string | Comentarios sobre el estado actual |
fechaCambioEstado | string | Fecha y hora del cambio |
Campos específicos para servicios completados
| Campo | Tipo | Descripción |
|---|---|---|
tarifaServicio | string | Tarifa final cobrada |
banderazo | string | Costo base del servicio |
distancia | string | Distancia total recorrida |
tiempo | string | Tiempo total empleado |
recargosAdicioneales | string | Costos adicionales |
recibido | string | Confirmación de recepción por cliente |
fechaRecibido | string | Fecha cuando cliente recibió servicio |
Campos para servicios incompletos
| Campo | Tipo | Descripción |
|---|---|---|
razonIncompleto | string | Motivo principal de incompletitud |
motivoIncompleto | string | Detalle específico del motivo |
Ejemplos de Payload
Central Acepta Servicio
json
{
"idServicio": "TSV202507030001",
"codigoEstado": 2,
"dniGestor": "98765432",
"detalleEstado": {
"nitPrestador": "900123456",
"nombrePrestador": "Central Norte SAS",
"telefonoCentral": "+57 4 123-4567",
"latitud": "6.2400",
"longitud": "-75.5800",
"idTecnico": "TEC001",
"telefonoTecnico": "+57 300-123-4567",
"tarifaEstimada": "85000",
"eta": "25",
"fechaCambioEstado": "2025-07-03T14:30:00Z"
}
}Técnico en Desplazamiento
json
{
"idServicio": "TSV202507030001",
"codigoEstado": 3,
"dniGestor": "98765432",
"detalleEstado": {
"nitPrestador": "900123456",
"nombrePrestador": "Central Norte SAS",
"latitud": "6.2420",
"longitud": "-75.5790",
"idTecnico": "TEC001",
"telefonoTecnico": "+57 300-123-4567",
"eta": "15",
"observaciones": "Técnico salió de la base, tiempo estimado 15 minutos",
"fechaCambioEstado": "2025-07-03T14:45:00Z"
}
}Técnico en Sitio
json
{
"idServicio": "TSV202507030001",
"codigoEstado": 4,
"dniGestor": "98765432",
"detalleEstado": {
"nitPrestador": "900123456",
"nombrePrestador": "Central Norte SAS",
"latitud": "6.2442",
"longitud": "-75.5812",
"idTecnico": "TEC001",
"telefonoTecnico": "+57 300-123-4567",
"observaciones": "Técnico llegó al sitio y contactó al cliente",
"fechaCambioEstado": "2025-07-03T15:00:00Z"
}
}Servicio Completado
json
{
"idServicio": "TSV202507030001",
"codigoEstado": 8,
"dniGestor": "98765432",
"detalleEstado": {
"nitPrestador": "900123456",
"nombrePrestador": "Central Norte SAS",
"idTecnico": "TEC001",
"tarifaServicio": "95000",
"banderazo": "35000",
"distancia": "12.5",
"tiempo": "45",
"recargosAdicioneales": "10000",
"informacionAdicional": "Servicio completado satisfactoriamente",
"recibido": "Sí",
"fechaRecibido": "2025-07-03T15:45:00Z",
"fechaCambioEstado": "2025-07-03T15:45:00Z"
}
}Servicio Incompleto
json
{
"idServicio": "TSV202507030001",
"codigoEstado": 6,
"dniGestor": "98765432",
"detalleEstado": {
"nitPrestador": "900123456",
"nombrePrestador": "Central Norte SAS",
"idTecnico": "TEC001",
"razonIncompleto": "Cliente no encontrado",
"motivoIncompleto": "Cliente no responde llamadas y no está en el sitio",
"observaciones": "Se realizaron 3 intentos de contacto sin respuesta",
"fechaCambioEstado": "2025-07-03T15:30:00Z"
}
}Servicio Cancelado
json
{
"idServicio": "TSV202507030001",
"codigoEstado": 7,
"dniGestor": "98765432",
"detalleEstado": {
"nitPrestador": "900123456",
"nombrePrestador": "Central Norte SAS",
"motivoCancelacion": "Cancelación solicitada por cliente",
"observaciones": "Cliente resolvió el problema por otros medios",
"fechaCambioEstado": "2025-07-03T14:50:00Z"
}
}Implementación del Endpoint
Node.js + Express
javascript
const express = require('express');
const app = express();
app.use(express.json());
app.put('/webhook/cambio-estado', (req, res) => {
try {
// 1. Validar API Key
const apiKey = req.headers['x-apikey'];
if (apiKey !== process.env.TSALVA_API_KEY) {
return res.status(401).json({ error: 'API Key inválida' });
}
// 2. Extraer datos del webhook
const { idServicio, codigoEstado, detalleEstado } = req.body;
// 3. Procesar según el estado
switch (codigoEstado) {
case 2: // Aceptado
procesarAceptacion(idServicio, detalleEstado);
break;
case 3: // En desplazamiento a origen
procesarTecnicoEnCamino(idServicio, detalleEstado);
break;
case 4: // En sitio
procesarTecnicoEnSitio(idServicio, detalleEstado);
break;
case 8: // Finalizado
procesarServicioCompletado(idServicio, detalleEstado);
break;
case 6: // Incompleto
procesarServicioIncompleto(idServicio, detalleEstado);
break;
case 7: // Cancelado
procesarServicioCancelado(idServicio, detalleEstado);
break;
default:
console.log(`Estado no manejado: ${codigoEstado}`);
}
// 4. Confirmar recepción
res.status(200).json({ received: true });
} catch (error) {
console.error('Error procesando webhook:', error);
res.status(500).json({ error: 'Error interno' });
}
});
function procesarAceptacion(idServicio, detalle) {
// Notificar al cliente que central aceptó
console.log(`Central ${detalle.nombrePrestador} aceptó servicio ${idServicio}`);
console.log(`Técnico: ${detalle.idTecnico}, ETA: ${detalle.eta} minutos`);
// Actualizar base de datos
// Enviar notificación push/SMS al usuario final
}
function procesarServicioCompletado(idServicio, detalle) {
// Procesar facturación
console.log(`Servicio ${idServicio} completado`);
console.log(`Tarifa final: $${detalle.tarifaServicio}`);
// Generar factura
// Enviar encuesta de satisfacción
}Python + Flask
python
from flask import Flask, request, jsonify
import os
app = Flask(__name__)
@app.route('/webhook/cambio-estado', methods=['PUT'])
def webhook_cambio_estado():
try:
# 1. Validar API Key
api_key = request.headers.get('x-apikey')
if api_key != os.environ.get('TSALVA_API_KEY'):
return jsonify({'error': 'API Key inválida'}), 401
# 2. Obtener datos
data = request.get_json()
id_servicio = data['idServicio']
codigo_estado = data['codigoEstado']
detalle_estado = data['detalleEstado']
# 3. Procesar cambio de estado
procesar_cambio_estado(id_servicio, codigo_estado, detalle_estado)
# 4. Confirmar recepción
return jsonify({'received': True}), 200
except Exception as e:
print(f'Error procesando webhook: {e}')
return jsonify({'error': 'Error interno'}), 500
def procesar_cambio_estado(id_servicio, codigo_estado, detalle):
if codigo_estado == 2: # Aceptado
print(f"Central {detalle['nombrePrestador']} aceptó servicio {id_servicio}")
elif codigo_estado == 8: # Finalizado
print(f"Servicio {id_servicio} completado con tarifa ${detalle['tarifaServicio']}")
elif codigo_estado == 3: # En desplazamiento a origen
print(f"Técnico en camino para servicio {id_servicio}")
elif codigo_estado == 4: # En sitio
print(f"Técnico llegó al sitio para servicio {id_servicio}")
elif codigo_estado == 6: # Incompleto
print(f"Servicio {id_servicio} incompleto: {detalle['razonIncompleto']}")
elif codigo_estado == 7: # Cancelado
print(f"Servicio {id_servicio} cancelado")
# ... otros estadosRespuestas Esperadas
Éxito
json
{
"received": true
}Código HTTP: 200 Correcto
Error de Autenticación
json
{
"error": "API Key inválida"
}Código HTTP: 401 No Autorizado (No autorizado)
Error Interno
json
{
"error": "Error interno del servidor"
}Código HTTP: 500 Error Interno del Servidor (Error interno del servidor)
Reintentos y Manejo de Errores
TSALVA reintentará el webhook en estos casos:
| Condición | Reintentos | Intervalos |
|---|---|---|
| Timeout (>30s) | 3 | 1min, 5min, 15min |
| Error 5xx | 3 | 1min, 5min, 15min |
| Error 4xx | 0 | No reintenta |
| Error 200 | 0 | Éxito, no reintenta |
Implementar Idempotencia
javascript
const procesadosRecientes = new Set();
app.put('/webhook/cambio-estado', (req, res) => {
const { idServicio, codigoEstado, detalleEstado } = req.body;
const fechaCambio = detalleEstado.fechaCambioEstado;
// Crear clave única para el evento
const eventoKey = `${idServicio}-${codigoEstado}-${fechaCambio}`;
// Verificar si ya procesamos este evento
if (procesadosRecientes.has(eventoKey)) {
console.log('Evento ya procesado, respondiendo con éxito');
return res.status(200).json({ received: true, processed: false });
}
// Procesar evento
procesarCambioEstado(req.body);
// Marcar como procesado (con TTL para limpiar memoria)
procesadosRecientes.add(eventoKey);
setTimeout(() => procesadosRecientes.delete(eventoKey), 60000);
res.status(200).json({ received: true, processed: true });
});Debugging
Para hacer debugging de webhooks, puedes usar servicios como webhook.site o ngrok para recibir las peticiones y ver su contenido.
Importante
Tu endpoint debe responder rápidamente (< 30 segundos). Para procesamientos largos, usa colas asíncronas y responde inmediatamente con 200 Correcto.
Referencia Rápida de Códigos
Para implementar correctamente el webhook, usa esta tabla de referencia:
javascript
// Constantes recomendadas para tu código
const ESTADOS_TSALVA = {
OFERTADO: 0,
RECURSO_ASIGNADO: 1,
ACEPTADO: 2,
EN_DESPLAZAMIENTO_ORIGEN: 3,
EN_SITIO: 4,
EN_DESPLAZAMIENTO_DESTINO: 5,
INCOMPLETO: 6,
CANCELADO: 7,
FINALIZADO: 8
};
// Función helper para obtener nombre del estado
function getNombreEstado(codigo) {
const nombres = {
0: 'Ofertado',
1: 'Recurso asignado',
2: 'Aceptado',
3: 'En desplazamiento a origen',
4: 'En sitio',
5: 'En desplazamiento al destino',
6: 'Incompleto',
7: 'Cancelado',
8: 'Finalizado'
};
return nombres[codigo] || 'Estado desconocido';
}Recomendación
Usa constantes en tu código para los códigos de estado en lugar de números mágicos. Esto hace que tu código sea más legible y mantenible.
Importante
Los códigos de estado son numéricos, no strings. Asegúrate de compararlos correctamente en tu código (usando === en JavaScript o == en Python).