ConectaPUI — Documentación de la API

Versión: 1.0 Base URL producción: https://api.conectapui.com/api/v1 Base URL sandbox: https://sandbox-api.conectapui.com/api/v1 Región AWS: us-west-2 (Oregon) Referencia normativa: Manual Técnico PUI v1.0 (DOF 13/01/2026)


Índice

  1. Arquitectura de autenticación
  2. Endpoints — Canal PUI (entrante)
  3. Endpoints — Plataforma (saliente)
  4. Endpoints — Dashboard institucional
  5. Códigos de error
  6. Cumplimiento de seguridad
  7. Bitácora de auditoría — Anexo 5

1. Arquitectura de autenticación

La API maneja dos canales de autenticación independientes:

┌─────────────────────────────────────────────────────────────┐
│  CANAL A — Institución → ConectaPUI (Dashboard)             │
│  Auth: JWT de AWS Cognito (Bearer token)                    │
│  Aplica a: /auth/*, /records/*, /uploads/*, /dashboard/*,   │
│            /tenants/*, /search/*, /referrals/*              │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  CANAL B — Servidor PUI → ConectaPUI (Integración gov.)     │
│  Auth: HS256 JWT propio (obtenido en /pui/login)            │
│  Aplica a: /pui/*                                           │
│  El "usuario" siempre debe ser exactamente "PUI" (3 chars)  │
│  TTL del token: 1 hora                                       │
└─────────────────────────────────────────────────────────────┘

Canal A — Cognito (Dashboard institucional)

Todas las rutas del dashboard requieren el header:

Authorization: Bearer <cognito-id-token>

El token Cognito contiene los claims custom:tenantId y custom:role. Se obtiene mediante el flujo estándar de Amplify/Cognito en el frontend.

Canal B — PUI JWT

El servidor de la PUI obtiene un token en dos pasos:

  1. POST /pui/login → recibe { token }
  2. Usa ese token como Authorization: Bearer <token> en llamadas subsecuentes

El token expira en 1 hora. La PUI debe reautenticar cuando recibe 401.


2. Endpoints — Canal PUI (entrante)

Estos endpoints son llamados por el servidor de la PUI hacia ConectaPUI. La URL base que debe registrar la PUI es: https://api.conectapui.com/api/v1/pui


POST /pui/login

Autenticación del servidor PUI. Emite un JWT de sesión.

Rate limit: 10 solicitudes / minuto / IP (protección anti-fuerza bruta) Autenticación: Ninguna (endpoint público)

Headers requeridos:

HeaderValorDescripción
X-Tenant-IdRFC de la instituciónIdentifica qué institución está siendo consultada
Content-Typeapplication/json

Body:

{
  "usuario": "PUI",
  "clave": "contraseña_secreta_de_la_institución"
}
CampoTipoRequeridoDescripción
usuariostringDebe ser exactamente "PUI" (3 caracteres). Cualquier otro valor devuelve 401
clavestringContraseña configurada por la institución en ConectaPUI

Respuesta exitosa 200:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Errores:

CódigoCausa
401usuario"PUI", clave incorrecta, o institución no configurada
429Más de 10 intentos por minuto desde la misma IP

Nota de seguridad: Los mensajes de error son idénticos en todos los casos de 401 ("Credenciales inválidas") para no revelar si el tenantId, el usuario o la clave son el campo incorrecto. Todos los intentos (exitosos y fallidos) se registran en la bitácora de auditoría con la IP de origen.


POST /pui/activar-reporte

La PUI notifica el ingreso de un nuevo reporte de persona desaparecida. Desencadena las fases 1 y 2 de búsqueda.

Autenticación: Bearer token obtenido en /pui/login Header requerido: X-Tenant-Id: <RFC>

Body:

{
  "id": "RPT-2024-00123",
  "curp": "XAXX010101HMCLRS00",
  "fechaDesaparicion": "2023-11-15",
  "nombre": "JUAN",
  "primerApellido": "PÉREZ",
  "segundoApellido": "GARCÍA",
  "sexo": "H",
  "descripcion": "Texto libre opcional"
}
CampoTipoRequeridoDescripción
idstringID único del reporte asignado por la PUI
curpstringCURP de 18 caracteres. Validado con regex ^[A-Z0-9]{18}$
fechaDesaparicionstring ISO 8601NoFecha de desaparición. Si se omite, se omite la fase 2 histórica
nombrestringNoNombre(s) de la persona
primerApellidostringNo
segundoApellidostringNo
sexostringNo
descripcionstringNoTexto libre, máximo 4096 chars almacenados

Lo que ocurre internamente:

1. Se almacena el reporte en DynamoDB con status="active"
2. Se encola en SQS → Fase 1 Worker (búsqueda inmediata en registro más reciente)
3. Se encola en SQS → Fase 2 Worker (búsqueda histórica hasta 12 años)
4. La Fase 3 se activa automáticamente cada hora vía EventBridge
5. Se registra en bitácora de auditoría (Anexo 5)

Respuesta exitosa 200:

{
  "message": "Reporte activado correctamente",
  "id": "RPT-2024-00123",
  "receivedAt": "2024-01-15T14:30:00.000Z"
}

POST /pui/activar-reporte-prueba

Idéntico a /pui/activar-reporte pero no dispara ninguna búsqueda real. Para validaciones en ambiente Sandbox.

El reporte se almacena con status="test" e isTest=true. La fase 3 no lo incluirá en búsquedas periódicas.

Respuesta exitosa 200:

{
  "message": "Reporte de prueba recibido correctamente",
  "id": "RPT-TEST-001",
  "receivedAt": "2024-01-15T14:30:00.000Z"
}

POST /pui/desactivar-reporte

La PUI notifica que un reporte debe cerrarse (persona localizada, caso resuelto, etc.). Detiene la fase 3 de búsqueda continua.

Autenticación: Bearer token obtenido en /pui/login Header requerido: X-Tenant-Id: <RFC>

Body:

{
  "id": "RPT-2024-00123",
  "motivo": "Persona localizada con vida"
}
CampoTipoRequeridoDescripción
idstringID del reporte a desactivar
motivostringNoRazón del cierre

Lo que ocurre internamente:

1. Se actualiza report.status = "inactive"
2. Se actualiza gsi1sk = "REPORT#INACTIVE"
   → El scheduler de Fase 3 ya no lo incluye (busca solo REPORT#ACTIVE)
3. Se registra en bitácora de auditoría

Respuesta exitosa 200:

{
  "message": "Reporte desactivado correctamente",
  "id": "RPT-2024-00123",
  "desactivadoAt": "2024-01-15T18:00:00.000Z"
}

3. Endpoints — Plataforma (saliente)

Estos endpoints son llamados por ConectaPUI hacia el servidor de la PUI, no por el cliente. Se documentan para referencia de la PUI y para auditoría.


POST <puiBaseUrl>/notificar-coincidencia

ConectaPUI llama este endpoint en el servidor de la PUI cuando encuentra un registro coincidente.

Cuándo se llama:

  • Fase 1: al encontrar el registro más reciente del CURP
  • Fase 2: por cada registro histórico en el período de desaparición
  • Fase 3: por cada nuevo registro detectado en búsqueda continua

Body enviado:

{
  "id_reporte": "RPT-2024-00123",
  "curp": "XAXX010101HMCLRS00",
  "fase_busqueda": "1",
  "lugar_nacimiento": "CDMX",
  "nombre": "JUAN",
  "primer_apellido": "PÉREZ",
  "segundo_apellido": "GARCÍA",
  "fecha_nacimiento": "1990-05-15",
  "sexo": "H",
  "telefono": "+525512345678",
  "correo": "juan@email.com",
  "domicilio": {
    "calle": "INSURGENTES SUR",
    "numero": "123",
    "colonia": "DEL VALLE",
    "cp": "03100",
    "municipio": "BENITO JUÁREZ",
    "entidad": "CIUDAD DE MÉXICO"
  }
}

Regla crítica — Fase 1: Los campos tipo_evento, fecha_evento, descripcion_lugar_evento y direccion_evento no se incluyen en fase 1 (Manual Técnico PUI v1.0, sección de Fase 1).

Fase 2 y 3: Se agregan los campos de evento:

{
  "...campos_anteriores": "...",
  "tipo_evento": "HOSPITALIZACION",
  "fecha_evento": "2023-10-01",
  "descripcion_lugar_evento": "Hospital General",
  "direccion_evento": "Av. Insurgentes Norte 1200"
}
CampoFase 1Fase 2/3Descripción
lugar_nacimiento✅ Siempre✅ SiempreDerivado automáticamente de los chars 11-12 del CURP
tipo_evento❌ Omitido✅ IncluidoTipo de interacción institucional
fecha_evento❌ Omitido✅ IncluidoFecha del evento
descripcion_lugar_evento❌ Omitido✅ Incluido
direccion_evento❌ Omitido✅ Incluido

Reintentos: 3 intentos con backoff exponencial en errores 5xx. En error 401, se renueva el token y se reintenta una vez.


POST <puiBaseUrl>/busqueda-finalizada

ConectaPUI notifica a la PUI que la búsqueda histórica (Fase 2) ha concluido.

Cuándo se llama: Siempre al terminar la Fase 2, independientemente de si se encontraron coincidencias. Si el reporte no tenía fechaDesaparicion, se llama inmediatamente.

Body:

{
  "id": "RPT-2024-00123",
  "institucion_id": "RFC_DE_LA_INSTITUCION"
}

4. Endpoints — Dashboard institucional

Todos estos endpoints requieren Authorization: Bearer <cognito-token>. El tenantId se extrae automáticamente del JWT — no se pasa por URL ni body.


Autenticación

POST /auth/register

Registra una nueva institución.

Body:

{
  "rfc": "XAXX010101000",
  "institutionName": "Mi Institución AC",
  "email": "admin@institucion.gob.mx",
  "password": "SecurePass123!",
  "phone": "+525512345678",
  "referralCode": "ABC123"
}

POST /auth/login

Inicia sesión de un usuario institucional.

{
  "email": "admin@institucion.gob.mx",
  "password": "SecurePass123!"
}

Respuesta: { "token": "...", "user": { ... } }


Configuración del tenant

GET /tenants/:tenantId

Obtiene la configuración de la institución. Los campos encryptedClave y encryptedToken nunca se devuelven.

Respuesta:

{
  "tenantId": "XAXX010101000",
  "rfc": "XAXX010101000",
  "institutionName": "Mi Institución AC",
  "email": "admin@institucion.gob.mx",
  "puiConfigured": true,
  "puiBaseUrl": "https://pui.institucion.gob.mx/api",
  "createdAt": "2024-01-01T00:00:00.000Z"
}

PATCH /tenants/:tenantId/pui-config

Actualiza las credenciales de integración PUI.

{
  "puiBaseUrl": "https://pui.institucion.gob.mx/api",
  "puiClave": "contraseña_minimo_8_chars"
}

La puiClave se cifra con AES-256-GCM antes de almacenarse. Nunca se persiste en texto claro ni se devuelve en ningún endpoint.


Padrón de registros

GET /records?limit=20

Lista los registros más recientes del padrón de la institución.

Respuesta:

{
  "items": [
    {
      "curp": "XAXX010101HMCLRS00",
      "nombre": "JUAN",
      "primerApellido": "PÉREZ",
      "fechaNacimiento": "1990-05-15",
      "sexo": "H",
      "lugarNacimiento": "CDMX",
      "updatedAt": "2024-01-15T10:00:00.000Z"
    }
  ]
}

GET /records/:curp

Obtiene el registro más reciente de un CURP específico. Devuelve 404 si no existe.

POST /records/search

Búsqueda flexible.

{
  "curp": "XAXX010101HMCLRS00"
}
{
  "fromDate": "2023-01-01",
  "toDate": "2023-12-31"
}

Carga de archivos Excel

POST /uploads/initiate

Solicita una URL pre-firmada de S3 para subir un archivo Excel.

{ "fileName": "registros_enero_2024.xlsx" }

Respuesta:

{
  "uploadUrl": "https://s3.amazonaws.com/...",
  "s3Key": "uploads/TENANT/2024-01-15/uuid.xlsx",
  "expiresIn": 300
}

POST /uploads/process

Inicia el procesamiento del archivo ya subido a S3.

{ "s3Key": "uploads/TENANT/2024-01-15/uuid.xlsx" }

El procesamiento es asíncrono — se encola en SQS y el worker lo procesa. El estado se puede consultar en el dashboard.


Flujo de búsqueda

GET /search/flow?status=active&limit=50

Lista todos los reportes con su estado de flujo actual.

Query paramValoresDefault
statusactive, inactive, testactive
limit1-10020

Respuesta:

{
  "items": [
    {
      "reportId": "RPT-2024-00123",
      "curp": "XAXX010101HMCLRS00",
      "nombre": "JUAN",
      "status": "active",
      "totalCoincidencias": 3,
      "fase1Coincidencias": 1,
      "fase2Coincidencias": 2,
      "fase3Coincidencias": 0,
      "steps": [
        { "id": "received",          "status": "completed", "label": "Reporte recibido de PUI" },
        { "id": "phase1_search",     "status": "completed", "label": "Fase 1 — Búsqueda de datos básicos" },
        { "id": "phase1_notify",     "status": "completed", "label": "Notificación fase 1 enviada a PUI" },
        { "id": "phase2_search",     "status": "completed", "label": "Fase 2 — Búsqueda histórica" },
        { "id": "phase2_notify",     "status": "completed", "label": "Coincidencias fase 2 notificadas" },
        { "id": "busqueda_finalizada","status": "completed","label": "Búsqueda histórica finalizada" },
        { "id": "phase3_continuous", "status": "running",   "label": "Fase 3 — Búsqueda continua activa" }
      ]
    }
  ],
  "total": 1
}

Los valores posibles de status por paso son: pending, running, completed, skipped, failed.

GET /search/flow/:reportId

Detalle de flujo de un reporte específico. Misma estructura que el ítem anterior.


Dashboard

GET /dashboard/stats

{
  "totalRecords": 15420,
  "totalUploads": 12,
  "activeReports": 7,
  "totalCoincidencias": 23,
  "recentActivity": [...]
}

GET /dashboard/pui-activity?limit=100

Historial de eventos PUI (entrantes y salientes). Cada ítem:

{
  "id": "uuid",
  "type": "notificar-coincidencia",
  "reportId": "RPT-2024-00123",
  "direction": "outbound",
  "timestamp": "2024-01-15T14:35:00.000Z",
  "description": "Coincidencia notificada al PUI"
}

Tipos de evento: login-exitoso, login-failed, activar-reporte, activar-reporte-prueba, desactivar-reporte, notificar-coincidencia, busqueda-finalizada.

GET /dashboard/api-logs?limit=100

Log de todas las llamadas HTTP. Cada ítem:

{
  "requestId": "uuid",
  "method": "POST",
  "url": "/api/v1/pui/activar-reporte",
  "statusCode": 200,
  "durationMs": 145,
  "timestamp": "2024-01-15T14:30:00.000Z",
  "direction": "inbound",
  "requestBody": { "...": "sanitizado" },
  "error": null
}

Los campos password, clave, token, secret, puiClave, puiToken, encryptedClave y encryptedToken nunca aparecen en los logs — son reemplazados por [REDACTED] antes de escribir.


5. Códigos de error

Todos los errores siguen el formato:

{
  "statusCode": 400,
  "message": "CURP must match ^[A-Z0-9]{18}$",
  "error": "Bad Request"
}
CódigoSignificadoCausa más común
400Bad RequestCampo requerido faltante o formato inválido (CURP, fecha, email)
401UnauthorizedToken ausente, expirado o inválido
403ForbiddenEl tenant del token no coincide con el recurso solicitado
404Not FoundRegistro o reporte no encontrado
409ConflictTenant ya registrado con ese RFC
429Too Many RequestsRate limit excedido (100/min global, 10/min en /pui/login)
500Internal Server ErrorError de infraestructura — revisar logs en CloudWatch

6. Cumplimiento de seguridad

6.1 SAST — Análisis estático de código

ControlImplementaciónUbicación
Validación de entradaclass-validator en todos los DTOs. CURP valida contra ^[A-Z0-9]{18}$. Fechas con @IsDateString(). Strings con @MaxLength().src/modules/*/dto/*.ts
InyecciónDynamoDB no usa SQL. Las queries usan ExpressionAttributeValues parametrizadas — no hay interpolación de strings en queries.src/common/dynamodb/dynamodb.service.ts
Secretos en códigoCero secretos en código. ENCRYPTION_KEY y PUI_JWT_SECRET se resuelven en tiempo de ejecución desde AWS Secrets Manager.src/common/utils/secrets.util.ts
JWT seguroAlgoritmo HS256 explícito. Verificación rechaza alg: none. TTL de 1 hora. Subject siempre validado (sub === "PUI").src/common/guards/pui-jwt.guard.ts
Cifrado de credencialespuiClave y puiToken almacenados con AES-256-GCM (IV aleatorio por operación). Nunca en texto claro.src/common/utils/crypto.util.ts
Error messages opacosTodos los 401 de /pui/login devuelven el mismo mensaje genérico, independientemente de cuál campo falló.src/modules/pui/pui.controller.ts
Dependencias@aws-sdk/* v3, axios v1.6+, class-validator v0.14+, jsonwebtoken v9. npm audit integrado en CI/CD.apps/backend/package.json

6.2 DAST — Pruebas dinámicas

ControlImplementaciónUbicación
HTTPS obligatorioHSTS con max-age=31536000; includeSubDomains; preload en todos los responsesCDK: conecta-pui-stack.ts
CORS restrictivoProducción: solo conectapui.com, www.conectapui.com, app.conectapui.com. Dev: solo localhost:3000. No AllOrigins.CDK: allowedOrigins por stage
Security headersX-Content-Type-Options: nosniff, X-Frame-Options: DENY, X-XSS-Protection: 1; mode=block, Referrer-Policy: strict-origin-when-cross-origin, Cache-Control: no-storeCDK: Gateway Responses
Rate limitingGlobal: 100 req/min/IP. /pui/login: 10 req/min/IP (brute-force). Aplicado via ThrottlerGuard global en NestJS.src/app.module.ts, src/modules/pui/pui.controller.ts
API Gateway throttling1,000 req/s rate, 2,000 burst. Primer nivel de defensa antes de llegar a Lambda.CDK: deployOptions.throttlingRateLimit
No log de datos sensiblesAPI GW dataTraceEnabled: false. Interceptor sanitiza campos sensibles antes de escribir a DynamoDB.CDK + api-logger.interceptor.ts

6.3 SCA — Análisis de composición de software

# Verificar vulnerabilidades en todas las dependencias
npm audit --workspaces

# Actualizar parches disponibles
npm audit fix --workspaces

Dependencias de riesgo identificadas:

PaqueteVersiónEstadoAcción
xlsx^0.18.5CVEs de prototype pollution conocidasMigrar a exceljs en siguiente sprint
aws-serverless-express^3.4.0DeprecatedReemplazado por @vendia/serverless-express (ya en deps) — remover el deprecated
Resto del stackSin CVEs activos al 2026-03-19Mantener actualizados en CI/CD

Política de CI/CD: El pipeline debe ejecutar npm audit --audit-level=high y fallar si hay vulnerabilidades de severidad Alta o Crítica.


7. Bitácora de auditoría — Anexo 5

ConectaPUI implementa el Anexo 5 del Manual Técnico PUI mediante dos tablas de registro independientes:

7.1 Tabla de eventos PUI (MAIN_TABLE, prefijo PUI_EVENT#)

Registra todas las interacciones con la PUI, tanto entrantes como salientes.

Estructura de cada registro:

{
  "pk": "PUI_EVENT#<uuid>",
  "sk": "REPORT#<reportId>",
  "gsi1pk": "TENANT#<tenantId>",
  "gsi1sk": "PUI_EVENT#<ISO-timestamp>",
  "eventId": "uuid-v4",
  "tenantId": "RFC_DE_LA_INSTITUCION",
  "reportId": "RPT-2024-00123",
  "eventType": "notificar-coincidencia",
  "direction": "outbound",
  "payload": "{\"fase\":\"1\",\"curp\":\"XAXX...\"}",
  "createdAt": "2024-01-15T14:35:00.000Z",
  "ttl": 1234567890
}

Eventos registrados:

EventoDirecciónCuándo
login-exitosoinboundPUI autenticó correctamente
login-failedinboundIntento fallido (incluye IP y razón: usuario_invalido, clave_invalida, tenant_no_encontrado)
activar-reporteinboundPUI envió un reporte real
activar-reporte-pruebainboundPUI envió un reporte de prueba
desactivar-reporteinboundPUI cerró un reporte
notificar-coincidenciaoutboundConectaPUI notificó una coincidencia a la PUI
busqueda-finalizadaoutboundConectaPUI notificó fin de búsqueda histórica

7.2 Tabla de logs de API (API_LOGS_TABLE)

Registra toda llamada HTTP tanto entrante (PUI → nosotros) como saliente (nosotros → PUI), incluyendo:

  • Timestamp exacto
  • Método HTTP y URL completa
  • Código de respuesta
  • Duración en milisegundos
  • IP del cliente (entrantes)
  • Request y response body (sanitizados — sin campos sensibles)
  • Mensaje de error si aplica

7.3 Retención y protección

CaracterísticaValor
TTL de registros90 días (campo ttl en DynamoDB con expiración automática)
InmutabilidadDynamoDB no tiene operación de DELETE masivo. Los registros de auditoría usan Put (append-only en la práctica)
AccesoSolo la Lambda con el rol IAM de la aplicación puede escribir/leer. No hay acceso público
Cifrado en reposoDynamoDB cifrado con AWS KMS (habilitado por defecto en todas las tablas)
Cifrado en tránsitoHTTPS/TLS 1.2+ obligatorio en todos los endpoints

7.4 Consultar la bitácora

Vía API (dashboard):

GET /dashboard/pui-activity?limit=100
GET /dashboard/api-logs?limit=100

Vía AWS CLI (auditoría directa):

aws dynamodb query \
  --table-name conectapui-prod-main \
  --index-name GSI1 \
  --key-condition-expression "gsi1pk = :pk AND begins_with(gsi1sk, :prefix)" \
  --expression-attribute-values '{
    ":pk":     {"S": "TENANT#<RFC>"},
    ":prefix": {"S": "PUI_EVENT#"}
  }' \
  --scan-index-forward false \
  --region us-west-2

Apéndice — Variables de entorno

VariableDescripciónFuente
MAIN_TABLETabla DynamoDB principalCDK output
UPLOADS_TABLETabla de cargasCDK output
API_LOGS_TABLETabla de logs APICDK output
UPLOADS_BUCKETBucket S3 para ExcelCDK output
USER_POOL_IDCognito User PoolCDK output
USER_POOL_CLIENT_IDCognito App ClientCDK output
PHASE1_QUEUE_URLSQS Fase 1CDK output
PHASE2_QUEUE_URLSQS Fase 2CDK output
PHASE3_QUEUE_URLSQS Fase 3CDK output
PUI_NOTIFICATION_QUEUE_URLSQS notificador PUICDK output
ENCRYPTION_KEY_SECRET_ARNARN secret AES-256CDK output
PUI_JWT_SECRET_ARNARN secret JWT PUICDK output

Para desarrollo local, usar ENCRYPTION_KEY y PUI_JWT_SECRET directamente como variables de entorno (sin Secrets Manager). Ver apps/backend/.env.example.