Problema
Registrar un cliente en el Perú empieza por un RUC o un DNI. Y ahí empieza el problema: el dato existe —SUNAT publica su padrón como dato abierto— pero cada sistema que lo necesita acaba resolviéndolo a su manera. Un scraper contra la consulta web que se rompe cuando cambia el HTML. Una API de la que nadie sabe de dónde saca los datos ni de cuándo son. O el caso más común: alguien teclea el nombre de la empresa a mano.
Lo descubrí resolviendo lo mismo tres veces, en tres sistemas distintos. El dolor no era «falta una API de RUC»: era que ninguna respondía de dónde venía el dato ni de qué fecha era, y eso es exactamente lo que pregunta un auditor.
Contexto
Cuando un RUC entra en una ficha de cliente deja de ser una consulta y pasa a ser un registro. Si mañana alguien revisa por qué esa factura salió a nombre de esa empresa, la respuesta no puede ser «lo trajo una API». Tiene que ser «salió del padrón de SUNAT del 27 de septiembre, con esta licencia».
Con el DNI el problema cambia de naturaleza: ya no es un registro público, es la identidad de una persona. Quien la consulta por cuenta de otro es encargado de su tratamiento, con todo lo que eso exige. Tratar los dos datos igual es lo cómodo. NexoID nació para no hacerlo.
Arquitectura
API-first y headless. No hay panel para el cliente final: lo que se entrega es un contrato OpenAPI 3.1, un token y una respuesta con meta. Los tokens de prueba (nxid_test_) no consumen cuota.
| Pieza | Qué hace |
|---|
| Laravel 13 + Octane sobre FrankenPHP | El runtime: el framework vive en memoria entre peticiones |
| PostgreSQL 16 | El padrón completo, los tenants, la auditoría y la facturación |
| PgBouncer | En modo transaction para el tráfico de la API. La ingesta y las migraciones van por conexión directa, porque el ETL sostiene un advisory lock de sesión que PgBouncer perdería al repartir transacciones |
| Redis ×2 | Contadores, rate limit y caché, en dos instancias con políticas de persistencia distintas (ver Decisiones clave) |
| Ingesta nocturna | El padrón se descarga cada madrugada; cada ficha guarda la fecha del archivo del que salió |
Cada respuesta lleva su propia trazabilidad:
"meta": {
"origen": "padron_reducido_sunat",
"fecha_dato": "2026-09-27",
"atribucion": "Padrón Reducido de Contribuyentes — SUNAT"
}
No es un campo decorativo: es lo que permite usar el dato en un documento fiscal.
Qué resuelve
Cuatro APIs públicas sobre un solo token, y una quinta bajo contrato:
- RUC. La ficha del contribuyente —estado, condición, ubigeo y actividad económica— desde el padrón de SUNAT, y la búsqueda por razón social. 18.4 millones de contribuyentes, con el 82.6 % de las fichas resueltas hasta el nombre del distrito.
- Tipo de cambio. El dólar y el euro oficiales del BCRP —con SUNAT de respaldo—: el de hoy, el de una fecha, el de un rango o el de un mes. No descuenta cuota.
- Ubigeo. Departamento, provincia y distrito a partir de un código INEI.
- Vigilancia de RUC. Un webhook firmado avisa cuando un RUC vigilado cambia de estado, de condición o de nombre. El cliente no tiene que preguntar: se le avisa.
- Identidad (DNI), solo por contrato. Exige el scope
dni.read, una finalidad declarada, una base legal y un anexo de tratamiento firmado. No se activa desde la web.
A eso se suman las dos cosas que vuelven integrable una API: lotes de hasta 100 documentos por petición e Idempotency-Key, para que una conexión cortada se repita sin consultar —ni cobrar— dos veces.
Decisiones clave
- El dato se clasifica por su origen, y la clase manda. Clase A es dato público de fuente oficial: el padrón, el BCRP. Clase B es identidad de terceros que llega de un proveedor externo. Clase C, identidad de una fuente oficial contratada. NexoID es titular de su copia de la Clase A y solo encargado de tratamiento de la B y la C: no las retiene ni las usa para nada más.
- Las dos instancias de Redis son un requisito de cumplimiento, no una optimización. La persistencia se configura por instancia, así que la del caché de Clase B corre sin disco (
appendonly no, save ""). Una identidad de terceros no puede acabar escrita en disco ni por accidente. Consolidarlas ahorraría un contenedor y rompería la garantía.
- El cumplimiento lo verifica el build. Persistir en PostgreSQL un DNI de Clase B o C lo rechaza una política de persistencia, y un test de arquitectura rompe el build si alguien lo intenta. Una regla legal que solo vive en un documento se olvida en el siguiente refactor.
- El régimen lo decide el contenido, no la ruta. Un RUC que empieza en 10, 15 o 17 es una persona natural:
GET /ruc/10… devuelve lo mismo que GET /dni/… y recibe el mismo trato. Sin el scope de identidad responde 403, la respuesta va private, no-store y la auditoría guarda el documento hasheado.
- Sin caché de borde delante. Cada consulta llega al servidor y queda registrada antes de responder. Un CDN haría la API más rápida y la auditoría incompleta; gana la auditoría.
- El error es parte del contrato. Código estable, mensaje en español y una señal de si se arregla esperando. Un
429 cuota_agotada es información; un 500 genérico es una llamada a soporte.
Lo que NexoID no hace es parte del diseño, no una lista de pendientes: no busca personas por nombre, no exporta personas naturales, no vende datos ni arma bases, y no puntúa ni perfila a nadie. En un dominio donde el dato es sensible, el alcance se define también por lo que se deja fuera.
Cómo se construye
Treinta y siete sprints, cada uno cerrado con un ADR que deja escrita la decisión y lo que costó. 1,648 casos de prueba en Pest 4, PHPStan en nivel 8 y CI en GitHub Actions. Va por la versión 1.22.
No es disciplina por estética. En un servicio que trata identidad de terceros, la pregunta «¿por qué está hecho así?» la puede hacer un auditor, y la respuesta tiene que estar escrita antes de que llegue.
Qué queda por delante
- La revisión legal. La política de privacidad, los términos, el anexo de tratamiento y el documento de seguridad ya están redactados y publicados. Falta la revisión de un abogado, que es lo que destraba el alta pública autoservicio: hoy el acceso se pide por WhatsApp.
- Los SDKs de PHP y JavaScript y el servidor MCP se entregan a pedido mientras se publican. El contrato OpenAPI ya es el que manda; los clientes tipados vienen detrás.
- Lo que viene: verificar que un nombre corresponde a un documento sin revelar el dato, y validar comprobantes electrónicos uno a uno o en lote.
Aprendizajes
El producto nació de la peor forma posible de descubrir una necesidad: resolviéndola tres veces. Lo que no vi hasta la tercera fue que el valor no estaba en consultar el RUC —eso lo hace cualquiera— sino en poder demostrar de dónde salió.
La segunda lección fue sobre el DNI. Tratar un RUC 10 como «otro RUC» parece razonable hasta que te das cuenta de que su nombre es el nombre de alguien. Decidir el régimen por el contenido del dato, y no por el endpoint que lo pidió, es lo que cierra la puerta trasera: pedir por RUC lo que no se puede pedir por DNI.
Resultado
17 centros podológicos y una clínica dental consultan con NexoID en producción desde septiembre de 2026, validando los datos de sus pacientes y clientes contra el padrón oficial. Los dos sistemas son míos —Podoplus y Diseña Estudio Dental— y fueron los que originaron el producto: el primer cliente de NexoID fue el problema que lo creó.