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.
Arquitectura del Sistema
El sistema está diseñado en torno a tres componentes principales que interactúan entre sí:
Tu Aplicación Cliente
Envía la solicitud de alta de contrato con todos los datos necesarios y la URL del callback.
API de Imagina Energía
Valida la solicitud, encola el procesamiento y devuelve un request_id inmediatamente.
Tu Servidor de Callback
Recibe la notificación con el resultado del proceso y gestiona la subida de documentos.
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
Esta URL la debes proporcionar en el campo callback_url
de cada solicitud de alta de contrato.
Flujo Completo del Proceso
El proceso completo de alta de contrato sigue estos pasos secuenciales:
Envío de Solicitud
Tu aplicación envía una petición POST con los datos del contrato y la URL de callback.
Validación Inmediata
La API valida el esquema y campos obligatorios. Si hay errores, devuelve 400.
Aceptación (202)
Si la validación es correcta, devuelve código 202 con un request_id.
Procesamiento en Segundo Plano
Se ejecuta: Credit Check → Alta de Contrato → Envío a Firma (en PRE devuelve error).
Callback con Resultado
Se envía POST a tu callback_url con el resultado completo del proceso.
Vertificación firma del request
Tu servidor de callback valida que la firma del request es realmente hecha por los sistemas de Imagina Energia
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
Para contratos de personas físicas (residenciales)
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 | |
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 |
callback_url en cada solicitud.
La URL puede ser diferente para cada petición según tus necesidades de enrutamiento.
Ejemplo de Solicitud
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 -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)
{
"message": "Request is being processed asynchronously",
"request_id": 12345
}
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_titulartipo_documento_titular (NIF/NIE) |
razon_social_titularnumero_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) |
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
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
{
"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
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)
- 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_idpara 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.
- 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:
{
"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:
{
"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 |
- Usa identificadores únicos y significativos para tu organización
- Documenta el formato de tus referencias para facilitar el soporte
- Guarda el
request_idjunto con tureferencia_externapara doble referencia - Considera incluir timestamp o secuencias para unicidad
Ejemplo en 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
@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.
Cómo se Genera la Firma
La API genera la firma del callback siguiendo este proceso:
Timestamp
Se genera un timestamp UNIX (segundos desde epoch) que se incluirá en los headers
Canonicalización del Payload
El payload JSON se serializa de forma determinista (claves ordenadas, sin espacios)
Construcción del Mensaje
Se construye el mensaje: {timestamp}.{url}.{payload_canonical}
Firma HMAC-SHA256
Se firma el mensaje usando HMAC-SHA256 con la clave semilla compartida
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
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
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;
}
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.
Integración en el Endpoint
@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:
Clave Semilla
Confirma que CALLBACK_SEED_KEY coincide exactamente con la proporcionada por Imagina Energía
Canonicalización del Payload
El JSON debe serializarse con claves ordenadas alfabéticamente y sin espacios: separators=(',',':')
URL Exacta
La URL usada en la verificación debe coincidir exactamente con la URL del callback (protocolo, host, path, query params)
Timestamp
Verifica que tu servidor tenga la hora correcta sincronizada (NTP)
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
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
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
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
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
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
- 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.
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)
📧 Email: serviciosistemas@imaginaenergia.com
📚 Documentación: API Swagger