Skip to content

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ódigoEstadoDescripción
0OfertadoEl servicio fue creado y distribuido a centrales
1Recurso asignadoUna central asignó un técnico al servicio
2AceptadoLa central aceptó el servicio oficialmente
3En desplazamiento a origenEl técnico se dirige al punto de origen
4En sitioEl técnico llegó al lugar del servicio
5En desplazamiento al destinoEl técnico se dirige al destino (si aplica)
6IncompletoEl servicio no pudo completarse
7CanceladoEl servicio fue cancelado
8FinalizadoEl servicio se completó exitosamente

Configuración Previa

Requisitos:

  1. URL del endpoint: Debe ser accesible públicamente
  2. API Key: Clave para validar autenticidad
  3. 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

CampoTipoDescripción
idServiciostringIdentificador único del servicio
codigoEstadonumberCódigo numérico del nuevo estado (ver tabla de códigos)
dniGestorstringDocumento del gestor que registra el cambio
detalleEstadoobjectInformación detallada del estado actual

Detalle del Estado

CampoTipoDescripción
nitPrestadorstringNIT del prestador asignado
nombrePrestadorstringRazón social del prestador
telefonoCentralstringTeléfono de contacto de la central
latitudstringUbicación actual del técnico (latitud)
longitudstringUbicación actual del técnico (longitud)
idTecnicostringIdentificador único del técnico
telefonoTecnicostringTeléfono directo del técnico
tarifaEstimadastringTarifa estimada para el servicio
etastringTiempo estimado de llegada (minutos)
observacionesstringComentarios sobre el estado actual
fechaCambioEstadostringFecha y hora del cambio

Campos específicos para servicios completados

CampoTipoDescripción
tarifaServiciostringTarifa final cobrada
banderazostringCosto base del servicio
distanciastringDistancia total recorrida
tiempostringTiempo total empleado
recargosAdicionealesstringCostos adicionales
recibidostringConfirmación de recepción por cliente
fechaRecibidostringFecha cuando cliente recibió servicio

Campos para servicios incompletos

CampoTipoDescripción
razonIncompletostringMotivo principal de incompletitud
motivoIncompletostringDetalle 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 estados

Respuestas 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ónReintentosIntervalos
Timeout (>30s)31min, 5min, 15min
Error 5xx31min, 5min, 15min
Error 4xx0No reintenta
Error 2000É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).

Tsalva API - Documentación desarrollada por RobPixels