endpoint.pe

Cómo consultar un RUC con Node.js y fetch

Consulta RUC con Node.js: fetch nativo con async/await, validación del dígito verificador, un endpoint en Express que oculta el token y manejo de errores.

eEquipo endpoint.pe•03 set. 20268 min de lectura

Tu aplicación en Node.js tiene un formulario donde el usuario escribe un RUC y espera que aparezca la razón social sin tener que tipearla. O tienes un proceso nocturno que revisa si tus proveedores siguen ACTIVOS y HABIDOS. En ambos casos el trabajo es el mismo: consultar el RUC en el padrón de SUNAT y leer la respuesta.

En esta guía vas a hacerlo con fetch nativo y async/await, sin instalar clientes HTTP. Primero validas el dígito verificador para no gastar consultas en números mal tipeados, luego escribes una función con manejo de errores y timeout, y por último la expones desde un endpoint en Express que oculta el token al navegador.

Los ejemplos usan la API de consulta RUC de endpoint.pe. Si ya probaste la llamada con curl, la traducción a JavaScript te va a resultar directa.


Lo que necesitas

Node.js 18 o superior. Desde esa versión fetch es global, así que no hace falta instalar nada para hacer peticiones HTTP. Para el endpoint usaremos Express, que instalas con npm install express. Los ejemplos están en módulos ES (import), así que agrega "type": "module" a tu package.json o usa la extensión .mjs.

También necesitas un token de la API. Si no tienes, crea una cuenta; incluye 150 consultas gratis al mes sin tarjeta. El token se lee desde process.env.ENDPOINT_PE_TOKEN. En desarrollo puedes ponerlo en un archivo .env y cargarlo con node --env-file=.env servidor.js (Node 20.6 o superior) o con el paquete dotenv. En producción lo defines en el entorno del servidor o del contenedor.

Nunca lo escribas en el código ni lo subas al repositorio. Si tienes dudas sobre cómo guardarlo bien, lee primero seguridad de tokens de API.

La llamada que vas a reproducir en Node es esta:

curl https://api.endpoint.pe/v1/ruc/20131312955 \
  -H "Authorization: Bearer TU_TOKEN"

Usamos el RUC de SUNAT como ejemplo porque es una entidad pública. La respuesta trae ruc, razon_social, estado, condicion, direccion y ubigeo.

Valida el dígito verificador antes de consultar

El último dígito del RUC se calcula a partir de los diez anteriores. Si no coincide, el número tiene un error de tipeo y no vale la pena enviarlo a la API: la respuesta va a ser 404 o, peor, un RUC distinto si el error cayó en una combinación válida. Validar localmente es gratis e instantáneo.

// ruc.js
export function rucTieneFormatoValido(ruc) {
  if (!/^(10|15|17|20)\d{9}$/.test(ruc)) return false;

  const pesos = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
  const suma = pesos.reduce((acc, peso, i) => acc + peso * Number(ruc[i]), 0);
  const resto = 11 - (suma % 11);
  const digito = resto === 10 ? 0 : resto === 11 ? 1 : resto;

  return digito === Number(ruc[10]);
}

La expresión regular exige 11 dígitos y uno de los prefijos que asigna SUNAT (10 para persona natural, 20 para persona jurídica, 15 y 17 para casos especiales). Después se multiplican los diez primeros dígitos por los pesos, se suma, se calcula 11 menos el residuo de dividir entre 11, y se ajustan los casos 10 y 11 a 0 y 1. El paso a paso con ejemplos numéricos está en cómo validar un RUC con el dígito verificador.

Prueba la función con 20131312955: debe devolver true. Cambia el último dígito y debe devolver false. Ese es el filtro que va antes de cualquier consulta.

La consulta con fetch y async/await

Con el formato validado, la consulta en sí son pocas líneas:

const respuesta = await fetch('https://api.endpoint.pe/v1/ruc/20131312955', {
  headers: {
    Authorization: `Bearer ${process.env.ENDPOINT_PE_TOKEN}`,
    Accept: 'application/json',
  },
});

console.log(respuesta.status);
console.log(await respuesta.json());

En un módulo ES puedes usar await en el nivel superior del archivo, así que esto corre tal cual con node --env-file=.env prueba.js. Si todo está bien, verás un 200 y un objeto con la razón social de SUNAT, estado ACTIVO y condición HABIDO.

Hay un detalle de fetch que confunde a quien viene de otras librerías: no lanza excepción cuando la API responde 404 o 500. Solo falla (rechaza la promesa) si no pudo completar la petición, por ejemplo sin conexión o por timeout. Un 404 es una respuesta exitosa desde el punto de vista de fetch; eres tú quien tiene que mirar respuesta.status o respuesta.ok y decidir.

Una función con manejo de errores y timeout

Para usarla en una aplicación real, la consulta necesita tres cosas que el snippet anterior no tiene: un timeout, una forma de distinguir "no existe" de "algo falló", y errores con información suficiente para registrarlos.

// consultarRuc.js
const BASE_URL = 'https://api.endpoint.pe/v1';

export class ErrorConsultaRuc extends Error {
  constructor(mensaje, status) {
    super(mensaje);
    this.name = 'ErrorConsultaRuc';
    this.status = status;
  }
}

export async function consultarRuc(ruc) {
  let respuesta;

  try {
    respuesta = await fetch(`${BASE_URL}/ruc/${ruc}`, {
      headers: {
        Authorization: `Bearer ${process.env.ENDPOINT_PE_TOKEN}`,
        Accept: 'application/json',
      },
      signal: AbortSignal.timeout(10_000),
    });
  } catch (error) {
    throw new ErrorConsultaRuc(`No se pudo conectar con la API: ${error.message}`, 0);
  }

  if (respuesta.status === 404) return null;
  if (respuesta.status === 401) throw new ErrorConsultaRuc('Token inválido o vencido', 401);
  if (respuesta.status === 402) throw new ErrorConsultaRuc('Sin créditos disponibles', 402);
  if (respuesta.status === 429) throw new ErrorConsultaRuc('Límite de velocidad superado', 429);
  if (!respuesta.ok) throw new ErrorConsultaRuc(`Respuesta inesperada: HTTP ${respuesta.status}`, respuesta.status);

  return (await respuesta.json()).data;
}

AbortSignal.timeout(10_000) cancela la petición si pasan diez segundos sin respuesta y hace que fetch rechace con un error, que capturamos y convertimos en ErrorConsultaRuc con status 0. Así, quien llama a la función siempre recibe una de tres cosas: un objeto con los datos, null cuando el RUC no está en el padrón, o un ErrorConsultaRuc con un status que explica qué pasó.

Esa separación importa en la práctica. Un null se muestra al usuario como "el RUC no existe" y no se cobra como consulta. Un 401 o 402 es un problema de tu cuenta, no del usuario: hay que avisar al equipo, no al cliente. Un 429 pide esperar y reintentar con pausa creciente. En manejo de errores y reintentos está el patrón de backoff completo; aquí lo dejamos fuera para que la función se lea de un vistazo.

Un endpoint en Express que oculta el token

El navegador nunca debe llamar a la API directamente, porque tendría que conocer el token y cualquiera podría leerlo en las herramientas de desarrollo. La solución es un endpoint en tu backend: el frontend le pide el RUC a tu servidor, y tu servidor, que sí tiene el token, consulta la API y devuelve solo lo necesario.

// servidor.js
import express from 'express';
import { rucTieneFormatoValido } from './ruc.js';
import { consultarRuc, ErrorConsultaRuc } from './consultarRuc.js';

const app = express();

app.get('/api/ruc/:ruc', async (req, res, next) => {
  const { ruc } = req.params;

  if (!rucTieneFormatoValido(ruc)) {
    return res.status(400).json({ error: 'formato_invalido', mensaje: 'El RUC no tiene un formato válido' });
  }

  try {
    const empresa = await consultarRuc(ruc);

    if (!empresa) {
      return res.status(404).json({ error: 'not_found', mensaje: 'No se encontró el RUC' });
    }

    return res.json({
      ruc: empresa.ruc,
      razonSocial: empresa.razon_social,
      estado: empresa.estado,
      condicion: empresa.condicion,
      direccion: empresa.direccion,
    });
  } catch (error) {
    if (error instanceof ErrorConsultaRuc) {
      console.error('Consulta RUC falló', { ruc, status: error.status, mensaje: error.message });
      return res.status(503).json({ error: 'servicio_no_disponible' });
    }
    return next(error);
  }
});

app.listen(3000, () => console.log('Servidor en http://localhost:3000'));

Desde el navegador, el frontend llama a tu ruta y no a la API externa:

const respuesta = await fetch(`/api/ruc/${ruc}`);

if (respuesta.status === 404) {
  mostrarError('El RUC no existe en el padrón de SUNAT');
} else if (respuesta.ok) {
  const empresa = await respuesta.json();
  campoRazonSocial.value = empresa.razonSocial;
}

Fíjate en que el endpoint repite la validación de formato aunque el frontend ya la haya hecho. Es intencional: cualquiera puede llamar a /api/ruc/123 con curl saltándose tu formulario, y no quieres gastar créditos ni exponer errores por eso. También convertimos cualquier ErrorConsultaRuc en un 503 genérico hacia el cliente y registramos el detalle solo en el servidor; el usuario no necesita saber que te quedaste sin créditos.

Un detalle de Express: en la versión 4, los errores lanzados dentro de un handler async no llegan solos al middleware de errores, por eso usamos next(error) en vez de relanzar. En Express 5 ya se propagan automáticamente, pero next(error) funciona en ambas versiones.

Caché, registro y pruebas

Con el endpoint funcionando, tres mejoras marcan la diferencia en producción.

Caché. El estado de un contribuyente no cambia cada minuto. Si el mismo RUC se consulta varias veces al día (por ejemplo, cada vez que se edita un cliente), guarda la respuesta en memoria o en Redis con un tiempo de vida de varias horas. Un Map con marca de tiempo basta para una sola instancia; para varias, Redis. Qué cachear y por cuánto tiempo lo detallamos en caché de consultas RUC y DNI.

Registro. Guarda en tus logs el RUC consultado, el código de respuesta y la duración. Nunca el token ni la cabecera Authorization. Con eso, cuando alguien reporte que "no carga la razón social", vas a saber en un minuto si fue un 404 legítimo, un 429 por exceso de peticiones o un 402 por créditos agotados.

Pruebas. Para testear consultarRuc sin red, reemplaza fetch global con una función simulada que devuelva objetos Response con los códigos que te interesan (200 con un cuerpo de ejemplo, 404 y 429). El test de cada rama se escribe en pocas líneas y te protege de romper el endpoint cuando cambies el formato de la respuesta.

En resumen

  • Node 18 o superior trae fetch global; no necesitas instalar un cliente HTTP.
  • Valida prefijo, longitud y dígito verificador antes de consultar: ahorra créditos y detecta errores de tipeo.
  • fetch no lanza excepción por 404 o 500; revisa status y ok tú mismo, y agrega timeout con AbortSignal.timeout.
  • Devuelve null cuando no existe y lanza un error con status cuando falla la integración.
  • Expón un endpoint en Express que tenga el token y repita la validación; el navegador solo habla con tu servidor.
  • Protege ese endpoint, cachea respuestas y registra códigos de estado sin el token.

Preguntas frecuentes

¿Puedo usar axios o node-fetch en lugar de fetch nativo?

Sí, la lógica es la misma. Ten en cuenta que axios sí lanza excepción en respuestas 4xx y 5xx por defecto, así que el 404 lo vas a capturar en el catch y no en un if. Con fetch nativo evitas una dependencia y el comportamiento es el estándar del navegador.

¿Por qué no llamar a la API directamente desde React o Vue?

Porque el token viajaría al navegador y cualquiera podría copiarlo desde las herramientas de desarrollo y gastar tus créditos. El frontend llama a tu backend, y tu backend llama a la API. Es una regla sin excepciones para cualquier credencial de pago.

¿Qué hago con un RUC que existe pero está en BAJA o NO HABIDO?

Depende de tu negocio, pero como mínimo muéstralo con claridad en el formulario y no lo trates igual que un ACTIVO y HABIDO. Facturar a un contribuyente en esa situación puede traerte problemas con el sustento de tus operaciones. Lee qué implica cada estado y condición antes de decidir si bloqueas el registro o solo adviertes.

¿Funciona en TypeScript?

Sin cambios de lógica. Agrega un tipo para la respuesta (ruc, razon_social, estado, condicion, direccion, ubigeo, todos string) y tipa consultarRuc para que devuelva una promesa de ese tipo o null. Si el compilador no reconoce fetch o AbortSignal.timeout, actualiza el paquete @types/node a una versión acorde con tu Node.

Deja de copiar datos a mano. Automatízalo hoy mismo

Crea tu cuenta, copia tu token y empieza hoy con 150 consultas gratis al mes.