Apariencia
Webhook de Cambio de Estado
TSALVA envía una notificación saliente hacia el endpoint configurado del cliente generador cada vez que cambia el estado de un servicio (técnico en camino, en sitio, finalizado, incompleto, cancelado).
Fuente
Documentado contra src/v3/controller/users.controller.js (updateApi y su mapeo de códigos de estado). La petición HTTP se realiza con la función requestApi (src/functions.js), que usa axios con un único intento.
Petición
PUT {url_api}{api_endpoint.actualizar}La URL de destino se compone de tu url_api (URL base) + la clave actualizar del JSON api_endpoint configurado en tu perfil de API. Ver Configuración.
Headers
Content-Type: application/json
x-apikey: <tu_api_key> # solo si tienes una apiKey configuradaINFO
El header x-apikey se incluye únicamente si tu perfil de API tiene una apiKey registrada.
Códigos de estado
El estado se envía en el campo codigoEstado (como cadena). Estos son los valores que el código genera realmente:
| Código | Significado | Origen en el código |
|---|---|---|
3 | En camino a origen (técnico asignado en ruta) | Tarea newStatus == 2 |
4 | En sitio (técnico llegó) | Tarea newStatus == 3 |
5 | Estado intermedio (tarea completada en servicios de varias tareas) | Tareas intermedias |
6 | Incompleto (servicio no completado) | Flujo de servicio incompleto |
7 | Cancelado | Flujo de cancelación |
8 | Finalizado (servicio completado) | Última tarea newStatus == 4 |
WARNING
El código 0 no se notifica: si codigoEstado es 0, updateApi retorna sin enviar nada. No hay evidencia en el código de que se envíen los códigos 1 ni 2 como notificación saliente hacia el cliente; la aceptación se comunica por el webhook de Aceptación de Oferta.
Estructura del payload
json
{
"idServicio": "TSV202507030001",
"codigoEstado": "4",
"dniGestor": "901668869",
"detalleEstado": {
"dniPrestador": "900123456",
"nombrePrestador": "CENTRAL NORTE SAS",
"telefonoCentral": "+57 4 123-4567",
"latitud": "6.2442",
"longitud": "-75.5812",
"dniTecnico": "12345678",
"telefonoTecnico": "+57 300-123-4567",
"tarifaEstimada": "85000",
"tarifaServicio": "95000",
"distancia": "12.5",
"recibido": true,
"tiempoLlegadaSitio": 0,
"observaciones": "",
"razonIncompleto": "",
"motivoIncompleto": "",
"banderazo": "",
"tiempo": "",
"recargosAdicioneales": "",
"informacionAdicional": "",
"fechaRecibido": "2025-07-03 14:30:00",
"fechaCambioEstado": "2025-07-03 15:00:00"
}
}Campos de nivel superior
| Campo | Tipo | Descripción |
|---|---|---|
idServicio | string | Identificador del servicio (serviceId o el UUID de la oferta) |
codigoEstado | string | Código del nuevo estado (ver tabla). Si no llega valor, se envía "3" |
dniGestor | string | Documento del gestor TSALVA (901668869; A900445812 para el NIT legacy 890903279) |
detalleEstado | object | Detalle del cambio de estado |
Objeto detalleEstado
| Campo | Tipo | Descripción |
|---|---|---|
dniPrestador | string | NIT del prestador (con dígito de verificación para el NIT legacy) |
nombrePrestador | string | Razón social del prestador |
telefonoCentral | string | Teléfono de la central |
latitud | string | Latitud del técnico |
longitud | string | Longitud del técnico |
dniTecnico | string | Documento del técnico (prefijado con C para el NIT legacy) |
telefonoTecnico | string | Teléfono del técnico |
tarifaEstimada | string | Tarifa estimada |
tarifaServicio | string | Tarifa final del servicio |
distancia | string | Distancia asociada al servicio |
recibido | boolean | Siempre true |
tiempoLlegadaSitio | number | Minutos estimados de llegada. Es 0 salvo cuando codigoEstado > 4, donde se calcula por distancia |
observaciones | string | Comentarios del cambio de estado |
razonIncompleto | string | Razón (cuando aplica a estado incompleto) |
motivoIncompleto | string | Motivo detallado (cuando aplica a estado incompleto) |
banderazo | string | Banderazo / costo base |
tiempo | string | Tiempo del servicio |
recargosAdicioneales | string | Recargos adicionales (nombre del campo tal cual lo envía la API) |
informacionAdicional | string | Información adicional |
fechaRecibido | string | Fecha de recepción de la oferta (YYYY-MM-DD HH:mm:ss) |
fechaCambioEstado | string | Fecha/hora del cambio de estado (YYYY-MM-DD HH:mm:ss) |
Campo recargosAdicioneales
El nombre del campo contiene una errata en el código de la API (recargosAdicioneales en vez de recargosAdicionales). Debes leerlo tal cual en tu integración.
Ejemplos de payload por estado
En sitio (código 4)
json
{
"idServicio": "TSV202507030001",
"codigoEstado": "4",
"dniGestor": "901668869",
"detalleEstado": {
"dniPrestador": "900123456",
"nombrePrestador": "CENTRAL NORTE SAS",
"latitud": "6.2442",
"longitud": "-75.5812",
"dniTecnico": "12345678",
"telefonoTecnico": "+57 300-123-4567",
"recibido": true,
"tiempoLlegadaSitio": 0,
"fechaCambioEstado": "2025-07-03 15:00:00"
}
}Finalizado (código 8)
json
{
"idServicio": "TSV202507030001",
"codigoEstado": "8",
"dniGestor": "901668869",
"detalleEstado": {
"dniPrestador": "900123456",
"nombrePrestador": "CENTRAL NORTE SAS",
"dniTecnico": "12345678",
"tarifaServicio": "95000",
"distancia": "12.5",
"recibido": true,
"fechaCambioEstado": "2025-07-03 15:45:00"
}
}Incompleto (código 6)
json
{
"idServicio": "TSV202507030001",
"codigoEstado": "6",
"dniGestor": "901668869",
"detalleEstado": {
"dniPrestador": "900123456",
"nombrePrestador": "CENTRAL NORTE SAS",
"dniTecnico": "12345678",
"razonIncompleto": "Cliente no encontrado",
"motivoIncompleto": "Cliente no responde y no está en el sitio",
"recibido": true,
"fechaCambioEstado": "2025-07-03 15:30:00"
}
}Respuesta esperada de tu endpoint
TSALVA considera exitosa la notificación cuando tu endpoint responde HTTP 200.
Comportamiento en error
Si tu endpoint responde con un código distinto de 200, TSALVA registra el error en sus logs pero no reintenta la notificación de cambio de estado (a diferencia del webhook de aceptación, aquí un fallo no revierte el servicio). El contenido del cuerpo de tu respuesta no se valida.
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) => {
// Verificación opcional del header x-apikey
if (req.headers['x-apikey'] !== process.env.TSALVA_API_KEY) {
return res.status(401).json({ error: 'API Key inválida' });
}
const { idServicio, codigoEstado, detalleEstado } = req.body;
switch (codigoEstado) {
case '3': console.log(`${idServicio}: técnico en camino`); break;
case '4': console.log(`${idServicio}: técnico en sitio`); break;
case '6': console.log(`${idServicio}: incompleto - ${detalleEstado.razonIncompleto}`); break;
case '7': console.log(`${idServicio}: cancelado`); break;
case '8': console.log(`${idServicio}: finalizado`); break;
default: console.log(`${idServicio}: estado ${codigoEstado}`);
}
res.status(200).json({ received: true });
});
app.listen(3000);Python + Flask
python
from flask import Flask, request, jsonify
import os
app = Flask(__name__)
@app.route('/webhook/cambio-estado', methods=['PUT'])
def cambio_estado():
if request.headers.get('x-apikey') != os.environ.get('TSALVA_API_KEY'):
return jsonify({'error': 'API Key inválida'}), 401
data = request.get_json()
codigo = data['codigoEstado']
detalle = data['detalleEstado']
if codigo == '8':
print(f"Servicio {data['idServicio']} finalizado, tarifa {detalle.get('tarifaServicio')}")
elif codigo == '6':
print(f"Servicio {data['idServicio']} incompleto: {detalle.get('razonIncompleto')}")
else:
print(f"Servicio {data['idServicio']} -> estado {codigo}")
return jsonify({'received': True}), 200Debugging
Para depurar puedes usar servicios como webhook.site o ngrok para inspeccionar el contenido de las peticiones.
Compara codigoEstado como cadena
codigoEstado viaja como string ("4", "8"…), no como número. Compáralo como texto en tu integración.