Integraciones

NexoCPE: facturación electrónica SUNAT como servicio

Servicio multi-tenant que emite comprobantes electrónicos ante SUNAT por API —facturas, boletas, notas y guías—, con cada emisor firmando con su propio certificado y el punto de venta sin esperar nunca a SUNAT.

NexoCPE: facturación electrónica SUNAT como servicio

Problema

Todo sistema que vende en el Perú tiene que emitir comprobantes electrónicos, y casi todos lo resuelven igual: conectándose a un facturador comercial. Funciona, hasta que se mira de cerca lo que pasa en la integración. El punto de venta espera a SUNAT dentro de la misma petición. Un reintento puede duplicar un comprobante. Un error de negocio llega como un 500 genérico.

Mis sistemas emitían así. Y no era mi primer encuentro con el dominio: en 2021 desarrollé la facturación electrónica de Mi Factura Perú. NexoCPE es esa experiencia convertida en producto: un servicio de emisión que se integra por API y que no le hace pagar al punto de venta los problemas de SUNAT.

No reemplaza al facturador que ya funciona donde está. Es un servicio aparte, para lo que se integra de aquí en adelante.

Contexto

NexoCPE emite bajo la modalidad SEE – Del Contribuyente. La distinción importa más de lo que parece: no es un OSE ni un PSE. Cada emisor firma con su propio certificado digital. Centralizar la firma con un certificado nuestro obligaría a inscribirse como PSE, con un capital mínimo de 150 UIT y certificación ISO 27001.

Y el ambiente de pruebas de SUNAT engaña. Beta valida estructura, no reglas de negocio: acepta cualquier certificado, incluso autofirmado, y no comprueba credenciales. Que un comprobante pase en beta no significa que producción lo acepte. Además, está prohibido usarla para pruebas de carga: SUNAT bloquea la IP.

Arquitectura

API → 202 Accepted: el punto de venta sigue
    → correlativo → XML UBL 2.1 → firma XMLDSig → PDF + QR
    → cola → SOAP → CDR de SUNAT
PiezaQué hace
API v1Responde 202 Accepted en menos de un segundo. Idempotency-Key obligatoria, rate limit y permisos por clave. Lotes de hasta 50, con la idempotencia por comprobante: si el proxy corta a los 60 s, el reintento no duplica la mitad ya emitida
CorrelativoSELECT … FOR UPDATE sobre la fila de la serie, en la misma transacción que crea el comprobante
XML y firmaUBL 2.1 firmado con XMLDSig. La librería de firma vive encapsulada en un solo módulo, detrás de una capa de abstracción
ColaHorizon sobre Redis: escalera de reintentos de 46 h, circuit breaker con contingencia y un proceso que recoge los envíos huérfanos
PDFA4 y ticket de 80 mm con el QR del manual, renderizados con Chrome headless
AislamientoRow Level Security de PostgreSQL. La aplicación conecta con un rol sin superusuario, porque un superusuario se salta la RLS siempre
Panel y portalPanel Filament multi-emisor, y un portal público para que el adquirente consulte su comprobante

Qué resuelve

  • Los cuatro comprobantes de venta: factura, boleta, nota de crédito y nota de débito. Las boletas van por resumen diario y se anulan con ConditionCode = 3; las facturas, por comunicación de baja. El sistema elige el mecanismo solo, y ese es el error de diseño más común de los puentes propios.
  • Los casos que complican el XML: detracción, percepción (en soles y en dólares), factura al crédito con cuotas, anticipos, ISC, exportación e ICBPER.
  • Guías de remisión, de remitente y de transportista, por la API REST de SUNAT.
  • El ciclo del certificado. Los .pfx se guardan cifrados con una clave derivada por emisor y nunca se descifran a disco; se rotan dentro de una transacción y avisan de su vencimiento a los 60, 30, 15, 7 y 1 días.
  • El alta del emisor, con el RUC comprobado por módulo 11 y un asistente que termina en «listo para BETA».
  • Partners: una sola clave para que un integrador o un estudio contable emita por toda su cartera de RUC, y nada fuera de ella.
  • Avisos al operador por correo y por Telegram, y un plugin para WooCommerce.

Decisiones clave

  • El punto de venta nunca espera a SUNAT. La emisión responde 202 y el envío ocurre en cola. Una caída de SUNAT es un comprobante pendiente, no una caja parada.
  • El XML firmado se guarda antes de enviarse, y después no se toca. Un reintento reenvía exactamente los mismos bytes. Volver a firmar está prohibido —cambiaría la firma y rompería la trazabilidad—, y cualquier alteración posterior, hasta reindentar, es el error 2335.
  • Un rechazo quema el número, y el sistema lo sabe. Un rechazo del rango 2000–3999 no se reintenta: se emite un comprobante nuevo. Los errores 0100–1999, los timeouts y el 0111 sí se reintentan con el mismo correlativo.
  • Los estados solo cambian con la respuesta de SUNAT. Una anulación no anula nada hasta que SUNAT la acepta.
  • El total lo calcula NexoCPE, no el cliente. Cálculo por línea con un tipo Monto, y lo que manda el punto de venta se compara al céntimo, no se emite. Si no coinciden, el ticket impreso no cuadraría con el comprobante.
  • Ningún secreto en un log. Ni la clave del certificado, ni la clave SOL, ni un token. La librería SOAP abría el TLS sin verificar el par si no se le decía nada, con la clave SOL viajando en el sobre: el transporte lo sobrescribe.

Hueco o quemado. Un número que el contador entregó y no tiene comprobante es un hueco: un defecto nuestro, que no debería poder pasar. Un comprobante rechazado, anulado o vencido es un número quemado: el sistema funcionando, pero el auditor va a preguntar por él. El reporte de saltos los separa, y falla solo ante un hueco. Un reporte que grita en falso deja de leerse, y entonces el hueco de verdad pasa desapercibido.

Cómo se construye

341 commits en cinco semanas, desde el 1 de septiembre de 2026, y va por la versión 0.31. Casi 2,000 casos de prueba en Pest: los de dominio corren sin base de datos, sin red y sin certificados, y la suite de aislamiento multi-tenant corre contra PostgreSQL real y es bloqueante. Ningún test puede salir a la red: un transporte de prueba hace fallar a cualquiera que lo intente.

No hay CI: el repositorio es privado y la puerta es local —Pint, PHPStan y la suite, en ese orden—. Por eso está escrita como regla y no como costumbre.

Qué queda por delante

  • Pasar a producción. Ya existe un RUC real con su certificado y su usuario SOL, pero se sigue en beta a propósito. El criterio de salida del núcleo es un mes emitiendo contra ese RUC, y el procedimiento del día D está escrito paso a paso.
  • El piloto con Podoplus, hoy en beta: las sedes están cargadas tal como SUNAT las tiene registradas en cada RUC, con una clave de API por sede.
  • Las reglas que entran en vigor el 1 de enero de 2027, ya empezadas.

Aprendizajes

Lo que más tiempo costó no estaba en ningún manual. El endpoint que lista el manual no es el del servicio: termina en ?wsdl, y si se postea el sobre ahí, SUNAT responde un error interno sin código. El código de error se pierde por el camino: PHP convierte el Client.3244 en un Client a secas, y un error que se reintenta se vuelve indistinguible de un bug propio. Y el texto del error 3244 habla del tipo de transacción, pero lo que falta es la forma de pago.

La lección de fondo fue sobre las fuentes. Contrastando la especificación contra la hoja oficial de reglas de SUNAT aparecieron nueve citas equivocadas, y una de ellas rechazaba facturas válidas. Por eso la regla del proyecto es una: no se inventan datos de SUNAT. La verdad está en la hoja oficial, no en la especificación ni en el código de otro sistema.

Y una de despliegue: de los seis contenedores, el que más se olvida es el scheduler. Sin él, las boletas se emiten y nunca se informan a SUNAT, los plazos vencen y los certificados caducan sin aviso. Todo con la API respondiendo 202 tan contenta.

Resultado

El núcleo emisor está completo y verificado contra beta, con CDR aceptada: factura, boleta, nota de crédito, resumen diario —incluida una anulación—, comunicación de baja y una boleta con ICBPER. Encima tiene panel, portal del adquirente, API de partners y plugin de WooCommerce. Lo que falta para producción no es código: es encender un RUC real.

Proyectos relacionados

IntegracionesEn producción

NexoID: los datos oficiales del Perú, con su procedencia

API multi-tenant que resuelve RUC, tipo de cambio y ubigeo desde las fuentes oficiales peruanas —y la identidad, bajo contrato—, con el origen, la fecha y la licencia de cada dato dentro de la respuesta.

Dos sistemas en producción consultando el padrón oficial desde septiembre de 2026

PHP 8.4Laravel 13FrankenPHPOctanePostgreSQL 16
Ver caso
DesarrolloEn staging

FarmaOrden: control operativo para cadenas de boticas

SaaS multi-tenant para farmacias y boticas del Perú, construido alrededor de una idea incómoda: el dueño de 20 locales no puede estar en todos, así que cada sol y cada caja de medicamento tienen que quedar atribuidos a una persona.

Veinte sesiones de construcción: el plan Cadena cerrado salvo el traslado entre RUC, con despliegue y observabilidad en pie

PHP 8.4Laravel 13PostgreSQL 16Redis 7Horizon
Ver caso