Skip to content

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 configurada

INFO

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ódigoSignificadoOrigen en el código
3En camino a origen (técnico asignado en ruta)Tarea newStatus == 2
4En sitio (técnico llegó)Tarea newStatus == 3
5Estado intermedio (tarea completada en servicios de varias tareas)Tareas intermedias
6Incompleto (servicio no completado)Flujo de servicio incompleto
7CanceladoFlujo de cancelación
8Finalizado (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

CampoTipoDescripción
idServiciostringIdentificador del servicio (serviceId o el UUID de la oferta)
codigoEstadostringCódigo del nuevo estado (ver tabla). Si no llega valor, se envía "3"
dniGestorstringDocumento del gestor TSALVA (901668869; A900445812 para el NIT legacy 890903279)
detalleEstadoobjectDetalle del cambio de estado

Objeto detalleEstado

CampoTipoDescripción
dniPrestadorstringNIT del prestador (con dígito de verificación para el NIT legacy)
nombrePrestadorstringRazón social del prestador
telefonoCentralstringTeléfono de la central
latitudstringLatitud del técnico
longitudstringLongitud del técnico
dniTecnicostringDocumento del técnico (prefijado con C para el NIT legacy)
telefonoTecnicostringTeléfono del técnico
tarifaEstimadastringTarifa estimada
tarifaServiciostringTarifa final del servicio
distanciastringDistancia asociada al servicio
recibidobooleanSiempre true
tiempoLlegadaSitionumberMinutos estimados de llegada. Es 0 salvo cuando codigoEstado > 4, donde se calcula por distancia
observacionesstringComentarios del cambio de estado
razonIncompletostringRazón (cuando aplica a estado incompleto)
motivoIncompletostringMotivo detallado (cuando aplica a estado incompleto)
banderazostringBanderazo / costo base
tiempostringTiempo del servicio
recargosAdicionealesstringRecargos adicionales (nombre del campo tal cual lo envía la API)
informacionAdicionalstringInformación adicional
fechaRecibidostringFecha de recepción de la oferta (YYYY-MM-DD HH:mm:ss)
fechaCambioEstadostringFecha/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}), 200

Debugging

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.

Tsalva API - Documentación desarrollada por RobPixels