Introducción

El proceso de alta de contratos en la API de Imagina Energía funciona mediante un sistema de procesamiento asíncrono con callbacks. Este diseño permite gestionar solicitudes complejas que requieren múltiples validaciones y operaciones sin bloquear al cliente.

📋 Aplicabilidad Universal El proceso descrito en este documento es común a todos los tipos de solicitudes de alta de contrato. Independientemente del tipo de contrato que solicites, la arquitectura y el flujo de trabajo son los mismos.

Arquitectura del Sistema

El sistema está diseñado en torno a tres componentes principales que interactúan entre sí:

1

Tu Aplicación Cliente

Envía la solicitud de alta de contrato con todos los datos necesarios y la URL del callback.

2

API de Imagina Energía

Valida la solicitud, encola el procesamiento y devuelve un request_id inmediatamente.

3

Tu Servidor de Callback

Recibe la notificación con el resultado del proceso y gestiona la subida de documentos.

⚠️ Requisito Fundamental Debes tener implementado un servidor de callback accesible públicamente para recibir las notificaciones del resultado del procesamiento.

Servidor de Callback

Tu servidor de callback es una pieza fundamental del proceso. Este servidor debe:

🌐 Requisitos del Servidor

  • Ser accesible públicamente: Tener una URL pública (HTTPS recomendado)
  • Aceptar peticiones POST: Recibir el resultado del procesamiento
  • Responder rápidamente: Confirmar recepción con código 200
  • Validar autenticidad del request: Confirmar que el request viene firmado por el sistema de Imagina Energia
  • Gestionar la subida de documentos: Realizar el upload de la documentación del contrato
  • Manejar errores: Implementar lógica de reintento si es necesario

Ejemplo de Endpoint de Callback

POST https://tuapp.ejemplo.com/webhooks/contrata

Esta URL la debes proporcionar en el campo callback_url de cada solicitud de alta de contrato.

🔒 Seguridad Las peticiones de callback incluyen una firma digital que puedes verificar para asegurar la autenticidad del origen. Consulta la documentación de seguridad para más detalles.

Flujo Completo del Proceso

El proceso completo de alta de contrato sigue estos pasos secuenciales:

1

Envío de Solicitud

Tu aplicación envía una petición POST con los datos del contrato y la URL de callback.

2

Validación Inmediata

La API valida el esquema y campos obligatorios. Si hay errores, devuelve 400.

3

Aceptación (202)

Si la validación es correcta, devuelve código 202 con un request_id.

4

Procesamiento en Segundo Plano

Se ejecuta: Credit Check → Alta de Contrato → Envío a Firma (en PRE devuelve error).

5

Callback con Resultado

Se envía POST a tu callback_url con el resultado completo del proceso.

6

Vertificación firma del request

Tu servidor de callback valida que la firma del request es realmente hecha por los sistemas de Imagina Energia

7

Subida de Documentos

Tu servidor de callback gestiona la subida de documentación asociada al contrato.

Solicitud de Alta de Contrato

Para solicitar el alta de un contrato, realiza una petición POST al endpoint correspondiente al tipo de contrato que deseas crear.

Endpoints Disponibles

POST https://pre-webhooks.imaginaenergia.com/contrato/residencial/c1

Para contratos de personas físicas (residenciales)

POST https://pre-webhooks.imaginaenergia.com/contrato/empresa/c1

Para contratos de empresas (personas jurídicas)

Campos Obligatorios Clave

Campo Descripción Ejemplo
callback_url URL de tu servidor para recibir el resultado https://tuapp.ejemplo.com/webhooks/contrata
canal_envio Canal para envío a firma: sms, email, email_otp email
referencia_externa Referencia opcional para identificar la petición (se devuelve en respuestas y callbacks) REF-2026-001234
Otros Otros campos requeridos con la información del contrato Mirar la documentgación Swagger
⚠️ Importante Debes incluir siempre el campo callback_url en cada solicitud. La URL puede ser diferente para cada petición según tus necesidades de enrutamiento.

Ejemplo de Solicitud

cURL
curl -X POST https://pre-webhooks.imaginaenergia.com/contrato/residencial/c1 \
  -H "Authorization: Bearer TU_TOKEN_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "cups": "ES0026000010979933FW",
    "provincia": "Madrid",
    "municipio": "Madrid",
    "cod_postal": "28001",
    "calle": "Calle de Alcalá",
    "numero_finca": "1",
    "tipo_via_cnmc": "Calle",
    "aclarador_finca": "2ºB",
    "potencia_contratada": [4600, 4600, 0, 0, 0, 0],
    "id_tarifa": 10350,
    "iban": "ES9121000418450200051332",
    "nombre_titular": "Juan",
    "primer_apellido_titular": "Pérez",
    "tipo_documento_titular": "NIF",
    "numero_documento_titular": "12345678A",
    "telefono_titular": "600000000",
    "prefijo_telefono_titular": "34",
    "email_titular": "juan@example.com",
    "provincia_titular": "Madrid",
    "municipio_titular": "Madrid",
    "cod_postal_titular": "28001",
    "calle_titular": "Calle de Alcalá",
    "numero_finca_titular": "1",
    "tipo_via_titular_cnmc": "Calle",
    "aclarador_finca_titular": "2ºB",
    "canal_envio": "email",
    "callback_url": "https://tuapp.ejemplo.com/webhooks/contrata",
    "referencia_externa": "REF-2026-001234"
  }'

Ejemplo de Solicitud - Empresa

cURL
curl -X POST https://pre-webhooks.imaginaenergia.com/contrato/empresa/c1 \
  -H "Authorization: Bearer TU_TOKEN_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "cups": "ES0026000010979933FW",
    "provincia": "Madrid",
    "municipio": "Madrid",
    "cod_postal": "28001",
    "calle": "Calle de Alcalá",
    "numero_finca": "1",
    "tipo_via_cnmc": "Calle",
    "aclarador_finca": "Oficina 2B",
    "potencia_contratada": [4600, 4600, 0, 0, 0, 0],
    "id_tarifa": 10350,
    "id_cnae": "9820",
    "iban": "ES9121000418450200051332",
    "razon_social_titular": "ACME CORPORATION S.L.",
    "numero_documento_titular": "B12345678",
    "telefono_titular": "910000000",
    "prefijo_telefono_titular": "34",
    "email_titular": "admin@acme.com",
    "provincia_titular": "Madrid",
    "municipio_titular": "Madrid",
    "cod_postal_titular": "28001",
    "calle_titular": "Calle de Alcalá",
    "numero_finca_titular": "1",
    "tipo_via_titular_cnmc": "Calle",
    "aclarador_finca_titular": "Oficina 2B",
    "nombre_firmante": "Juan",
    "primer_apellido_firmante": "Pérez",
    "tipo_documento_firmante": "NIF",
    "numero_documento_firmante": "12345678A",
    "telefono_firmante": "600000000",
    "prefijo_telefono_firmante": "34",
    "email_firmante": "juan.perez@acme.com",
    "canal_envio": "email",
    "callback_url": "https://tuapp.ejemplo.com/webhooks/contrata",
    "referencia_externa": "REF-2026-001234"
  }'

Respuesta Inmediata (202 Accepted)

JSON
{
  "message": "Request is being processed asynchronously",
  "request_id": 12345
}
✅ request_id Este identificador único es fundamental para el seguimiento de tu petición. Guárdalo para poder rastrear el estado y vincular el callback con la solicitud original.

Diferencias entre Residencial y Empresa

Aspecto Residencial C1 Empresa C1
Tipo de persona Física Jurídica (empresa)
Identificación titular nombre_titular + primer_apellido_titular
tipo_documento_titular (NIF/NIE)
razon_social_titular
numero_documento_titular (NIF)
Firmante El propio titular Persona física diferente
(campos *_firmante)
CNAE Opcional Obligatorio (id_cnae)
Validaciones No permite NIF de empresas Requiere NIF de empresa + datos firmante
Contactos 1 contacto (titular) 2 contactos (Administrador + Firmante)
💡 Nota importante Para empresas, los campos prefijo_telefono_titular y prefijo_telefono_firmante son obligatorios y se validan al recibir la petición.

Proceso en Segundo Plano

Una vez aceptada la solicitud, el sistema de Imagina Energia ejecuta un proceso completo en segundo plano que consta de tres fases principales:

🔍 Fase 1: Credit Check

Objetivo: Verificar la solvencia del titular del contrato.

  • Se consultan los datos del titular en sistemas de información crediticia
  • Se evalúa el riesgo según políticas internas
  • Resultado: Aprobado o Rechazado

📝 Fase 2: Alta de Contrato

Objetivo: Crear el contrato en el sistema Neuro.

  • Se validan los datos contra tablas maestras (provincias, municipios, tipos de vía, etc.)
  • Se crea el registro del cliente si no existe
  • Se genera el contrato con todos sus atributos técnicos
  • Se configura la domiciliación bancaria (IBAN, BIC/SWIFT, mandato)
  • Se asocia el punto de suministro con el CUPS proporcionado

Resultado: Contrato creado con ID y código únicos, en estado "Pendiente - Pendiente sin revisar" y sin fecha de firma.

✍️ Fase 3: Envío a Firma Electrónica

Objetivo: Iniciar el proceso de firma digital del contrato.

  • Se genera el documento contractual en formato PDF
  • Se envía al titular por el canal especificado (canal_envio)
  • Se crea el flujo de firma electrónica
⚠️ Importante - Entorno PRE En el entorno de PRE-PRODUCCIÓN, esta fase siempre devuelve error ya que el servicio de firma electrónica no está activo. Esto es normal y esperado.

Resultado: Contrato creado con ID y código únicos, en estado "Pendiente - Pendiente sin revisar" y con fecha de firma.

Recepción del Callback

Una vez completado el proceso en segundo plano, la API realiza una petición POST a la URL que especificaste en callback_url.

Estructura del Callback

JSON
{
  "request_id": 408,
  "referencia_externa": "REF-2026-001234",
  "credit_result": {
    "status_code": 200,
    "result_operation": "Aprobado"
  },
  "contrato_result": {
    "result_operation": "OK",
    "content": {
      "id": 543856,
      "codigo": "2511000868",
      "estado": {
        "descripcion": "Pendiente"
      },
      "subestado": {
        "subestado": "pendiente sin revisar",
        "descripcion": "Pendiente sin revisar"
      },
      "cliente": {
        "identificador": "12345678A",
        "nombre": "Juan",
        "primer_apellido": "Pérez",
        "correo_electronico": "juan@example.com",
        "telefono_1": "600000000"
      },
      "punto_suministro": {
        "cups": "ES0026000010979933FW"
      },
      "atributos": {
        "tarifa_atr": {
          "codigo": "018",
          "descripcion": "2.0TD"
        },
        "potencia_contratada": "[4600,4600,0,0,0,0]"
      },
      "iban": "ES9121000418450200051332",
      "bic_swift": "CAIXESBBXXX",
      "codigo_mandato": "14462511000868000001"
    }
  },
  "firma_result": {
    "result_code": null,
    "result_operation": "ERROR(Error sending contract for signature...)"
  }
}

Implementación del Endpoint de Callback

Python - Flask
from flask import Flask, request, jsonify
import logging

app = Flask(__name__)
logging.basicConfig(level=logging.INFO)

@app.route('/webhooks/contrata', methods=['POST'])
def callback_contrata():
    try:
        # Recibir el resultado
        data = request.get_json()
        request_id = data.get('request_id')
        
        logging.info(f"Callback recibido para request_id: {request_id}")
        
        # Extraer resultados
        credit_result = data.get('credit_result', {})
        contrato_result = data.get('contrato_result', {})
        firma_result = data.get('firma_result', {})
        
        # Verificar la firma del request de callback
        if not verificar_firma_callback(request):
            logging.error(f"Firma inválida para request_id: {request_id}")
            return jsonify({"status": "error", "message": "Invalid signature"}), 401

        # Verificar si el contrato fue creado exitosamente
        if contrato_result.get('result_operation') != 'OK':
            logging.error(f"Error en alta de contrato para {request_id}")
            return jsonify({"status": "received"}), 200
        
        # Extraer información del contrato
        contrato = contrato_result.get('content', {})
        contrato_id = contrato.get('id')
        codigo_contrato = contrato.get('codigo')
        
        logging.info(f"Contrato creado: ID={contrato_id}, Código={codigo_contrato}")
        
        # Aquí es donde debes subir la documentación
        subir_documentacion_contrato(contrato_id, request_id)
        
        return jsonify({"status": "received", "processed": True}), 200
        
    except Exception as e:
        logging.error(f"Error procesando callback: {e}")
        return jsonify({"status": "error", "message": str(e)}), 500

def subir_documentacion_contrato(contrato_id, request_id):
    """
    Función que gestiona la subida de documentación
    Implementación en la siguiente sección
    """
    pass

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
💡 Buenas Prácticas
  • Responde rápidamente con código 200 para confirmar recepción
  • Procesa la lógica pesada de forma asíncrona (colas, workers)
  • Guarda el request_id para trazabilidad
  • Implementa logging detallado para debugging
  • Valida la firma digital del callback para seguridad

Referencia Externa

El parámetro referencia_externa es un campo opcional que permite a los clientes incluir su propia referencia o identificador único para rastrear y correlacionar las peticiones con sus sistemas internos.

🎯 Ventajas de Usar Referencia Externa
  • Facilita la trazabilidad entre tu sistema y el de Imagina Energía
  • Permite correlacionar peticiones con tus propios identificadores de negocio
  • Se devuelve tanto en respuestas síncronas como en callbacks asíncronos
  • Ayuda en auditorías y seguimiento de operaciones

Cómo Usar Referencia Externa

Simplemente incluye el campo referencia_externa en tu solicitud de alta de contrato:

JSON Request
{
  "cups": "ES0026000010979933FW",
  "provincia": "Madrid",
  // ... otros campos obligatorios ...
  "callback_url": "https://tuapp.ejemplo.com/webhooks/contrata",
  "referencia_externa": "PEDIDO-2026-001234"
}

En la Respuesta del Callback

El valor de referencia_externa será devuelto en la respuesta, permitiendo correlacionar fácilmente el callback con tu petición original:

JSON Response
{
  "request_id": 408,
  "referencia_externa": "PEDIDO-2026-001234",
  "credit_result": {
    "status_code": 200,
    "result_operation": "Aprobado"
  },
  "contrato_result": {
    "result_operation": "OK",
    "content": {
      "id": 543856,
      "codigo": "2511000868"
      // ...
    }
  }
}

Casos de Uso Típicos

Caso de Uso Ejemplo de Referencia Descripción
ID de Pedido PEDIDO-2026-001234 Vincula el contrato con un pedido en tu sistema
ID de Cliente CLIENTE-789456 Asocia el contrato con el ID interno de tu cliente
Número de Expediente EXP-2026/0123 Útil para seguimiento administrativo
UUID Único 550e8400-e29b-41d4-a716-446655440000 Identificador universal único para correlación exacta
✅ Mejores Prácticas
  • Usa identificadores únicos y significativos para tu organización
  • Documenta el formato de tus referencias para facilitar el soporte
  • Guarda el request_id junto con tu referencia_externa para doble referencia
  • Considera incluir timestamp o secuencias para unicidad

Ejemplo en Python

Python
import requests
import uuid
from datetime import datetime

# Generar referencia externa única
referencia = f"CONTRATO-{datetime.now().strftime('%Y%m%d')}-{uuid.uuid4().hex[:8]}"

headers = {
    "Authorization": f"Bearer {jwt_token}",
    "Content-Type": "application/json"
}

payload = {
    "cups": "ES0026000010979933FW",
    # ... otros campos ...
    "callback_url": "https://tuapp.ejemplo.com/webhooks/contrata",
    "referencia_externa": referencia  # Tu referencia única
}

# Guardar la referencia en tu base de datos
db.save_contrato_request({
    'referencia_externa': referencia,
    'timestamp': datetime.now(),
    'status': 'pending'
})

# Enviar solicitud
response = requests.post(
    "https://pre-webhooks.imaginaenergia.com/contrato/residencial/c1",
    headers=headers,
    json=payload
)

if response.status_code == 202:
    result = response.json()
    # Actualizar con el request_id del sistema
    db.update_contrato_request(referencia, {
        'request_id': result['request_id']
    })
    print(f"✅ Solicitud enviada:")
    print(f"   Referencia Externa: {referencia}")
    print(f"   Request ID: {result['request_id']}")

Manejo en el Callback

Python
@app.route('/webhooks/contrata', methods=['POST'])
def recibir_callback():
    data = request.json
    
    # Recuperar ambos identificadores
    request_id = data.get('request_id')
    referencia_externa = data.get('referencia_externa')
    
    # Buscar en tu sistema usando tu propia referencia
    contrato_local = db.get_by_referencia_externa(referencia_externa)
    
    if contrato_local:
        # Actualizar con los resultados del callback
        contrato_local.update({
            'request_id': request_id,
            'credit_status': data['credit_result']['result_operation'],
            'contrato_id': data['contrato_result']['content']['id'],
            'contrato_codigo': data['contrato_result']['content']['codigo'],
            'status': 'completed'
        })
        
        logger.info(f"✅ Callback procesado para referencia: {referencia_externa}")
        return jsonify({'status': 'ok'}), 200
    else:
        logger.error(f"❌ Referencia externa no encontrada: {referencia_externa}")
        return jsonify({'error': 'Not found'}), 404

Verificación de Firma del Callback

Para garantizar la autenticidad e integridad de los callbacks que recibes, la API firma cada petición utilizando HMAC-SHA256. Debes verificar esta firma antes de procesar el callback.

⚠️ Seguridad Crítica La verificación de firma es esencial para proteger tu endpoint contra ataques de replay, suplantación o manipulación de datos. Nunca proceses un callback sin validar su firma.

Cómo se Genera la Firma

La API genera la firma del callback siguiendo este proceso:

1

Timestamp

Se genera un timestamp UNIX (segundos desde epoch) que se incluirá en los headers

2

Canonicalización del Payload

El payload JSON se serializa de forma determinista (claves ordenadas, sin espacios)

3

Construcción del Mensaje

Se construye el mensaje: {timestamp}.{url}.{payload_canonical}

4

Firma HMAC-SHA256

Se firma el mensaje usando HMAC-SHA256 con la clave semilla compartida

5

Codificación Base64

El digest se codifica en Base64 URL-safe y se envía en el header X-Signature

Headers de Firma

Los callbacks incluyen estos headers para la verificación:

Header Descripción Ejemplo
X-Signature Firma HMAC-SHA256 en formato v1={signature} v1=abc123...
X-Signature-Timestamp Timestamp UNIX en segundos 1699876543
X-Signature-Algorithm Algoritmo de firma usado HS256

Implementación de la Verificación

Python

Python
import hmac
import hashlib
import base64
import time
import json
from flask import request

def verificar_firma_callback(request_obj):
    """
    Verifica la firma HMAC-SHA256 de un callback recibido.
    
    Args:
        request_obj: Objeto request de Flask con headers y body
        
    Returns:
        bool: True si la firma es válida, False en caso contrario
    """
    # 1. Obtener la clave semilla (debe ser la misma que usa la API)
    seed = os.getenv('CALLBACK_SEED_KEY')
    if not seed:
        logging.error("CALLBACK_SEED_KEY no configurada")
        return False
    
    # 2. Extraer headers de firma
    signature_header = request_obj.headers.get('X-Signature', '')
    timestamp_header = request_obj.headers.get('X-Signature-Timestamp', '')
    algorithm = request_obj.headers.get('X-Signature-Algorithm', '')
    
    if not signature_header or not timestamp_header:
        logging.error("Headers de firma ausentes")
        return False
    
    # 3. Validar que el algoritmo sea el esperado
    if algorithm != 'HS256':
        logging.error(f"Algoritmo no soportado: {algorithm}")
        return False
    
    # 4. Extraer la firma (formato: v1=signature)
    if not signature_header.startswith('v1='):
        logging.error("Formato de firma inválido")
        return False
    
    received_signature = signature_header[3:]  # Remover 'v1='
    
    # 5. Protección contra replay attacks (opcional pero recomendado)
    # Rechazar callbacks con timestamp muy antiguo (ej: > 5 minutos)
    current_time = int(time.time())
    callback_time = int(timestamp_header)
    if abs(current_time - callback_time) > 300:  # 5 minutos
        logging.error(f"Callback demasiado antiguo o futuro: {callback_time}")
        return False
    
    # 6. Obtener el payload y canonicalizarlo
    try:
        payload = request_obj.get_json()
        canonical_payload = json.dumps(
            payload, 
            separators=(',', ':'), 
            sort_keys=True, 
            ensure_ascii=False
        )
    except Exception as e:
        logging.error(f"Error al parsear payload: {e}")
        return False
    
    # 7. Construir el mensaje a verificar
    # Formato: {timestamp}.{url}.{payload_canonical}
    url = request_obj.url
    message = f"{timestamp_header}.{url}.{canonical_payload}"
    
    # 8. Calcular la firma esperada
    key = seed.encode('utf-8')
    message_bytes = message.encode('utf-8')
    digest = hmac.new(key, message_bytes, hashlib.sha256).digest()
    expected_signature = base64.urlsafe_b64encode(digest).decode('ascii').rstrip('=')
    
    # 9. Comparación segura de firmas (constante en tiempo)
    is_valid = hmac.compare_digest(expected_signature, received_signature)
    
    if not is_valid:
        logging.error("Firma inválida")
        logging.debug(f"Esperada: {expected_signature}")
        logging.debug(f"Recibida: {received_signature}")
    
    return is_valid

Node.js

JavaScript
const crypto = require('crypto');

function verificarFirmaCallback(req) {
    // 1. Obtener la clave semilla
    const seed = process.env.CALLBACK_SEED_KEY;
    if (!seed) {
        console.error('CALLBACK_SEED_KEY no configurada');
        return false;
    }
    
    // 2. Extraer headers
    const signatureHeader = req.headers['x-signature'] || '';
    const timestampHeader = req.headers['x-signature-timestamp'] || '';
    const algorithm = req.headers['x-signature-algorithm'] || '';
    
    if (!signatureHeader || !timestampHeader) {
        console.error('Headers de firma ausentes');
        return false;
    }
    
    // 3. Validar algoritmo
    if (algorithm !== 'HS256') {
        console.error(`Algoritmo no soportado: ${algorithm}`);
        return false;
    }
    
    // 4. Extraer firma (formato v1=signature)
    if (!signatureHeader.startsWith('v1=')) {
        console.error('Formato de firma inválido');
        return false;
    }
    
    const receivedSignature = signatureHeader.substring(3);
    
    // 5. Protección contra replay attacks
    const currentTime = Math.floor(Date.now() / 1000);
    const callbackTime = parseInt(timestampHeader);
    if (Math.abs(currentTime - callbackTime) > 300) {  // 5 minutos
        console.error(`Callback demasiado antiguo: ${callbackTime}`);
        return false;
    }
    
    // 6. Canonicalizar payload
    const payload = req.body;
    const canonicalPayload = JSON.stringify(payload, Object.keys(payload).sort());
    
    // 7. Construir mensaje
    const url = req.protocol + '://' + req.get('host') + req.originalUrl;
    const message = `${timestampHeader}.${url}.${canonicalPayload}`;
    
    // 8. Calcular firma esperada
    const hmac = crypto.createHmac('sha256', seed);
    hmac.update(message);
    const digest = hmac.digest();
    const expectedSignature = digest.toString('base64url').replace(/=/g, '');
    
    // 9. Comparación segura
    const isValid = crypto.timingSafeEqual(
        Buffer.from(expectedSignature),
        Buffer.from(receivedSignature)
    );
    
    if (!isValid) {
        console.error('Firma inválida');
    }
    
    return isValid;
}
🔑 Obtención de la Clave Semilla La clave CALLBACK_SEED_KEY es proporcionada por Imagina Energía al configurar tu integración. Debe almacenarse de forma segura como variable de entorno y nunca incluirse en el código fuente.
⏰ Ventana de Tiempo Se recomienda implementar una ventana de validez para el timestamp (ejemplo: 5 minutos). Esto previene ataques de replay donde un atacante podría intentar reenviar un callback legítimo capturado anteriormente.

Integración en el Endpoint

Python - Ejemplo Completo
@app.route('/webhooks/contrata', methods=['POST'])
def callback_contrata():
    # PRIMERO: Verificar la firma antes de procesar
    if not verificar_firma_callback(request):
        logging.error("Firma de callback inválida - Rechazando petición")
        return jsonify({
            "status": "error", 
            "message": "Invalid signature"
        }), 401
    
    # Ahora es seguro procesar el callback
    try:
        data = request.get_json()
        request_id = data.get('request_id')
        
        logging.info(f"✅ Callback verificado para request_id: {request_id}")
        
        # Procesar normalmente...
        contrato_result = data.get('contrato_result', {})
        
        if contrato_result.get('result_operation') == 'OK':
            contrato_id = contrato_result.get('content', {}).get('id')
            subir_documentacion_contrato(contrato_id, request_id)
        
        return jsonify({"status": "received", "processed": True}), 200
        
    except Exception as e:
        logging.error(f"Error procesando callback: {e}")
        return jsonify({"status": "error", "message": str(e)}), 500

Debugging y Troubleshooting

Si la verificación de firma falla, verifica estos puntos:

1
Clave Semilla

Confirma que CALLBACK_SEED_KEY coincide exactamente con la proporcionada por Imagina Energía

2
Canonicalización del Payload

El JSON debe serializarse con claves ordenadas alfabéticamente y sin espacios: separators=(',',':')

3
URL Exacta

La URL usada en la verificación debe coincidir exactamente con la URL del callback (protocolo, host, path, query params)

4
Timestamp

Verifica que tu servidor tenga la hora correcta sincronizada (NTP)

5
Encoding

Asegúrate de usar UTF-8 para codificar el mensaje antes de firmarlo

Subida de Documentos

Una parte fundamental del proceso es que tu servidor de callback debe encargarse de subir la documentación del contrato una vez creado exitosamente.

Endpoint de Subida de Documentos

POST https://pre-webhooks.imaginaenergia.com/documento

Content-Type: multipart/form-data

Campos Requeridos

Campo Tipo Descripción
id_contrato string ID del contrato (obtenido del callback)
fichero binary Archivo del documento (PDF, imagen, etc.)
tipo_fichero string Tipo de documento (ej: "Contrato", "DNI", etc.)
fecha_documento date Fecha del documento (opcional)

Implementación de Subida

Python
import requests
from pathlib import Path

def subir_documentacion_contrato(contrato_id, request_id):
    """
    Sube los documentos asociados a un contrato
    """
    api_url = "https://pre-webhooks.imaginaenergia.com/documento"
    jwt_token = obtener_token_jwt()  # Tu función de autenticación
    
    # Lista de documentos a subir
    documentos = [
        {
            "ruta": "/path/to/contrato.pdf",
            "tipo": "Contrato",
            "fecha": "2025-11-13"
        },
        {
            "ruta": "/path/to/dni.pdf",
            "tipo": "Identificador Cliente",
            "fecha": "2025-11-13"
        }
    ]
    
    headers = {
        "Authorization": f"Bearer {jwt_token}"
    }
    
    for doc in documentos:
        try:
            # Preparar el archivo
            with open(doc["ruta"], 'rb') as f:
                files = {
                    'fichero': (Path(doc["ruta"]).name, f, 'application/pdf')
                }
                
                data = {
                    'id_contrato': str(contrato_id),
                    'tipo_fichero': doc["tipo"],
                    'fecha_documento': doc["fecha"]
                }
                
                # Subir documento
                response = requests.post(
                    api_url,
                    headers=headers,
                    files=files,
                    data=data
                )
                
                if response.status_code == 200:
                    result = response.json()
                    logging.info(
                        f"Documento '{doc['tipo']}' subido exitosamente para contrato {contrato_id}"
                    )
                else:
                    logging.error(
                        f"Error subiendo documento '{doc['tipo']}': {response.text}"
                    )
                    
        except Exception as e:
            logging.error(f"Error al subir documento: {e}")
            # Implementar lógica de reintento si es necesario
⚠️ Importante Asegúrate de gestionar correctamente los errores de subida. Si un documento falla, implementa lógica de reintento y notificación para no perder documentación.

Seguimiento de Peticiones

El request_id es fundamental para el seguimiento de tus peticiones a lo largo de todo el proceso.

Usos del request_id

🔍 Trazabilidad Completa

  • Identificación única: Cada solicitud tiene un ID incremental único
  • Vinculación callback: El callback incluye el mismo request_id de la solicitud
  • Logs y auditoría: Permite rastrear toda la operación en logs
  • Soporte técnico: Facilita la investigación de incidencias
  • Gestión interna: Puedes asociarlo con tus propios IDs de pedido/cliente

Ejemplo de Sistema de Seguimiento

Python
import sqlite3
from datetime import datetime

class SeguimientoContratos:
    def __init__(self):
        self.conn = sqlite3.connect('contratos.db')
        self.crear_tabla()
    
    def crear_tabla(self):
        self.conn.execute('''
            CREATE TABLE IF NOT EXISTS solicitudes (
                request_id INTEGER PRIMARY KEY,
                cliente_id TEXT,
                cups TEXT,
                fecha_solicitud TIMESTAMP,
                estado TEXT,
                contrato_id INTEGER,
                codigo_contrato TEXT,
                credit_check TEXT,
                fecha_callback TIMESTAMP,
                documentos_subidos INTEGER DEFAULT 0
            )
        ''')
        self.conn.commit()
    
    def registrar_solicitud(self, request_id, cliente_id, cups):
        """Registra una nueva solicitud"""
        self.conn.execute('''
            INSERT INTO solicitudes 
            (request_id, cliente_id, cups, fecha_solicitud, estado)
            VALUES (?, ?, ?, ?, ?)
        ''', (request_id, cliente_id, cups, datetime.now(), 'PENDIENTE'))
        self.conn.commit()
    
    def actualizar_callback(self, request_id, contrato_id, codigo, credit_check):
        """Actualiza con los datos del callback"""
        self.conn.execute('''
            UPDATE solicitudes 
            SET contrato_id = ?,
                codigo_contrato = ?,
                credit_check = ?,
                fecha_callback = ?,
                estado = ?
            WHERE request_id = ?
        ''', (contrato_id, codigo, credit_check, datetime.now(), 
              'CONTRATO_CREADO', request_id))
        self.conn.commit()
    
    def registrar_documento_subido(self, request_id):
        """Incrementa contador de documentos subidos"""
        self.conn.execute('''
            UPDATE solicitudes 
            SET documentos_subidos = documentos_subidos + 1
            WHERE request_id = ?
        ''', (request_id,))
        self.conn.commit()
    
    def obtener_estado(self, request_id):
        """Consulta el estado de una solicitud"""
        cursor = self.conn.execute('''
            SELECT * FROM solicitudes WHERE request_id = ?
        ''', (request_id,))
        return cursor.fetchone()

# Uso
seguimiento = SeguimientoContratos()

# Al enviar solicitud
seguimiento.registrar_solicitud(
    request_id=12345,
    cliente_id="CLI001",
    cups="ES0026000010979933FW"
)

# Al recibir callback
seguimiento.actualizar_callback(
    request_id=12345,
    contrato_id=543856,
    codigo="2511000868",
    credit_check="Aprobado"
)

Tipos de Contrato Disponibles

El sistema está diseñado para soportar múltiples tipos de contratos. El proceso descrito en este documento aplica a todos los tipos, solo cambia el endpoint y algunos campos específicos del tipo de contrato.

Estado Actual

✅ Disponible Ahora

Contrato Residencial C1

  • Endpoint: /contrato/residencial/c1
  • Descripción: Alta de contrato residencial C1 sin cambios de contrato, salvo el de comercializadora
  • Características:
    • Para clientes residenciales (persona física) con tarifa 2.0TD, 3.0TD, etc
    • Incluye credit check
    • Gestión automática de potencias contratadas
    • Soporte para autoconsumo (opcional)

Contrato Empresa C1

  • Endpoint: /contrato/empresa/c1
  • Descripción: Alta de contrato empresa C1 sin cambios de contrato, salvo el de comercializadora
  • Características:
    • Para personas jurídicas (empresas) con tarifa 2.0TD, 3.0TD, etc
    • Requiere razón social y NIF de empresa
    • Requiere datos del firmante (persona física autorizada)
    • CNAE obligatorio
    • Incluye credit check
    • Gestión automática de potencias contratadas
🔜 Próximamente Se irán habilitando más tipos de contratos siguiendo el mismo proceso:
  • Contratos C2: Con cambios en el contrato además del cambio de comercializadora
  • Contratos A3: Nuevo punto de suministro

Arquitectura Común

Independientemente del tipo de contrato, todos siguen la misma arquitectura:

  • ✅ Procesamiento asíncrono con callbacks
  • ✅ Generación de request_id único
  • ✅ Respuesta inmediata 202 Accepted
  • ✅ Callback con resultado completo
  • ✅ Subida de documentación posterior

Ejemplo Completo End-to-End

Este ejemplo muestra una implementación completa del proceso de alta de contrato, desde la solicitud hasta la subida de documentos.

Python - Sistema Completo
import requests
import logging
from flask import Flask, request, jsonify
from typing import Optional
import os
import hmac
import hashlib
import base64
import json
import time
from datetime import datetime

# Configuración
logging.basicConfig(level=logging.INFO)
app = Flask(__name__)

# Variables de entorno
API_BASE_URL = "https://pre-webhooks.imaginaenergia.com"
JWT_TOKEN = os.getenv("API_JWT_TOKEN")
CALLBACK_SEED_KEY = os.getenv("CALLBACK_SEED_KEY")

def verificar_firma_callback(request_obj):
    """
    Verifica la firma HMAC-SHA256 de un callback recibido.
    
    Args:
        request_obj: Objeto request de Flask con headers y body
        
    Returns:
        bool: True si la firma es válida, False en caso contrario
    """
    # 1. Obtener la clave semilla
    seed = CALLBACK_SEED_KEY
    if not seed:
        logging.error("CALLBACK_SEED_KEY no configurada")
        return False
    
    # 2. Extraer headers de firma
    signature_header = request_obj.headers.get('X-Signature', '')
    timestamp_header = request_obj.headers.get('X-Signature-Timestamp', '')
    algorithm = request_obj.headers.get('X-Signature-Algorithm', '')
    
    if not signature_header or not timestamp_header:
        logging.error("Headers de firma ausentes")
        return False
    
    # 3. Validar algoritmo
    if algorithm != 'HS256':
        logging.error(f"Algoritmo no soportado: {algorithm}")
        return False
    
    # 4. Extraer la firma
    if not signature_header.startswith('v1='):
        logging.error("Formato de firma inválido")
        return False
    
    received_signature = signature_header[3:]
    
    # 5. Protección contra replay attacks
    current_time = int(time.time())
    callback_time = int(timestamp_header)
    if abs(current_time - callback_time) > 300:  # 5 minutos
        logging.error(f"Callback demasiado antiguo: {callback_time}")
        return False
    
    # 6. Canonicalizar payload
    try:
        payload = request_obj.get_json()
        canonical_payload = json.dumps(
            payload, 
            separators=(',', ':'), 
            sort_keys=True, 
            ensure_ascii=False
        )
    except Exception as e:
        logging.error(f"Error al parsear payload: {e}")
        return False
    
    # 7. Construir mensaje
    url = request_obj.url
    message = f"{timestamp_header}.{url}.{canonical_payload}"
    
    # 8. Calcular firma esperada
    key = seed.encode('utf-8')
    message_bytes = message.encode('utf-8')
    digest = hmac.new(key, message_bytes, hashlib.sha256).digest()
    expected_signature = base64.urlsafe_b64encode(digest).decode('ascii').rstrip('=')
    
    # 9. Comparación segura
    is_valid = hmac.compare_digest(expected_signature, received_signature)
    
    if not is_valid:
        logging.error("Firma inválida")
    
    return is_valid

class GestorContratos:
    """Gestor completo del ciclo de vida de contratos"""
    
    def __init__(self, jwt_token: str):
        self.jwt_token = jwt_token
        self.base_url = API_BASE_URL
        self.seguimiento = {}
    
    def solicitar_alta_contrato(self, datos_contrato: dict, 
                               callback_url: str) -> Optional[int]:
        """
        Envía solicitud de alta de contrato
        Retorna el request_id si es exitoso
        """
        url = f"{self.base_url}/contrato/residencial/c1"
        
        # Agregar callback_url 
        datos_contrato['callback_url'] = callback_url

        
        headers = {
            "Authorization": f"Bearer {self.jwt_token}",
            "Content-Type": "application/json"
        }
        
        try:
            response = requests.post(url, json=datos_contrato, headers=headers)
            
            if response.status_code == 202:
                data = response.json()
                request_id = data['request_id']
                
                # Registrar en seguimiento
                self.seguimiento[request_id] = {
                    'estado': 'PENDIENTE',
                    'datos': datos_contrato,
                    'fecha_solicitud': datetime.now()
                }
                
                logging.info(f"✅ Solicitud aceptada. request_id: {request_id}")
                return request_id
            else:
                logging.error(f"❌ Error en solicitud: {response.status_code} - {response.text}")
                return None
                
        except Exception as e:
            logging.error(f"❌ Error de conexión: {e}")
            return None
    
    def procesar_callback(self, callback_data: dict) -> bool:
        """
        Procesa el callback recibido del API
        """
        request_id = callback_data.get('request_id')
        
        if request_id not in self.seguimiento:
            logging.warning(f"⚠️  request_id {request_id} no encontrado en seguimiento")
            return False
        
        # Extraer resultados
        credit_result = callback_data.get('credit_result', {})
        contrato_result = callback_data.get('contrato_result', {})
        
        # Verificar credit check
        if credit_result.get('result_operation') != 'Aprobado':
            self.seguimiento[request_id]['estado'] = 'RECHAZADO_CREDIT'
            logging.warning(f"⚠️  Credit check rechazado para request_id {request_id}")
            return False
        
        # Verificar alta de contrato
        if contrato_result.get('result_operation') != 'OK':
            self.seguimiento[request_id]['estado'] = 'ERROR_CONTRATO'
            logging.error(f"❌ Error en alta de contrato para request_id {request_id}")
            return False
        
        # Extraer datos del contrato
        contrato = contrato_result.get('content', {})
        contrato_id = contrato.get('id')
        codigo_contrato = contrato.get('codigo')
        
        # Actualizar seguimiento
        self.seguimiento[request_id].update({
            'estado': 'CONTRATO_CREADO',
            'contrato_id': contrato_id,
            'codigo_contrato': codigo_contrato,
            'fecha_callback': datetime.now()
        })
        
        logging.info(f"✅ Contrato creado: ID={contrato_id}, Código={codigo_contrato}")
        
        # Subir documentación
        self.subir_documentacion(contrato_id, request_id)
        
        return True
    
    def subir_documentacion(self, contrato_id: int, request_id: int):
        """
        Sube los documentos asociados al contrato
        """
        url = f"{self.base_url}/documento"
        
        headers = {
            "Authorization": f"Bearer {self.jwt_token}"
        }
        
        # Lista de documentos a subir (ejemplo)
        documentos = [
            {
                "path": f"./documentos/{request_id}/contrato.pdf",
                "tipo": "Contrato"
            },
            {
                "path": f"./documentos/{request_id}/dni.pdf",
                "tipo": "Identificador Cliente"
            }
        ]
        
        docs_subidos = 0
        
        for doc in documentos:
            try:
                if not os.path.exists(doc["path"]):
                    logging.warning(f"⚠️  Documento no encontrado: {doc['path']}")
                    continue
                
                with open(doc["path"], 'rb') as f:
                    files = {
                        'fichero': (os.path.basename(doc["path"]), f, 'application/pdf')
                    }
                    
                    data = {
                        'id_contrato': str(contrato_id),
                        'tipo_fichero': doc["tipo"]
                    }
                    
                    response = requests.post(url, headers=headers, 
                                           files=files, data=data)
                    
                    if response.status_code == 200:
                        docs_subidos += 1
                        logging.info(f"✅ Documento '{doc['tipo']}' subido")
                    else:
                        logging.error(f"❌ Error subiendo '{doc['tipo']}': {response.text}")
                        
            except Exception as e:
                logging.error(f"❌ Error al procesar documento: {e}")
        
        # Actualizar seguimiento
        self.seguimiento[request_id]['documentos_subidos'] = docs_subidos
        
        if docs_subidos == len(documentos):
            self.seguimiento[request_id]['estado'] = 'COMPLETADO'
            logging.info(f"✅ Proceso completado para request_id {request_id}")
        else:
            self.seguimiento[request_id]['estado'] = 'COMPLETADO_PARCIAL'
            logging.warning(f"⚠️  Proceso completado parcialmente para request_id {request_id}")

# Instancia global del gestor
gestor = GestorContratos(JWT_TOKEN)

# Endpoint para recibir callbacks
@app.route('/webhooks/contrata', methods=['POST'])
def webhook_contrata():
    """Endpoint que recibe los callbacks del API"""
    
    # PASO 1: Verificar firma ANTES de procesar
    if not verificar_firma_callback(request):
        logging.error("❌ Firma de callback inválida - Rechazando petición")
        return jsonify({
            "status": "error",
            "message": "Invalid signature"
        }), 401
    
    try:
        data = request.get_json()
        request_id = data.get('request_id')
        
        logging.info(f"📨 Callback verificado y recibido para request_id: {request_id}")
        
        # Procesar callback
        success = gestor.procesar_callback(data)
        
        return jsonify({
            "status": "received",
            "processed": success
        }), 200
        
    except Exception as e:
        logging.error(f"❌ Error procesando callback: {e}")
        return jsonify({
            "status": "error",
            "message": str(e)
        }), 500

# Endpoint de ejemplo para solicitar alta
@app.route('/api/alta-contrato', methods=['POST'])
def api_alta_contrato():
    """Endpoint interno para solicitar alta de contrato"""
    try:
        datos = request.get_json()
        
        # URL de callback (debe ser accesible públicamente)
        callback_url = "https://tuapp.ejemplo.com/webhooks/contrata"
        
        # Solicitar alta
        request_id = gestor.solicitar_alta_contrato(datos, callback_url)
        
        if request_id:
            return jsonify({
                "success": True,
                "request_id": request_id,
                "message": "Solicitud enviada correctamente"
            }), 202
        else:
            return jsonify({
                "success": False,
                "message": "Error al enviar solicitud"
            }), 500
            
    except Exception as e:
        return jsonify({
            "success": False,
            "message": str(e)
        }), 500

if __name__ == '__main__':
    logging.info("🚀 Iniciando servidor de gestión de contratos")
    app.run(host='0.0.0.0', port=5000)
💬 ¿Necesitas Ayuda? Para dudas sobre el proceso de contratación, problemas técnicos o soporte:

📧 Email: serviciosistemas@imaginaenergia.com
📚 Documentación: API Swagger