endpoint.pe

Cómo consultar un RUC con PHP y Laravel

Consulta RUC con PHP: ejemplo con curl nativo y con el cliente HTTP de Laravel, un servicio reutilizable, validación en un FormRequest y manejo del 404.

eEquipo endpoint.pe•24 ago. 20268 min de lectura

Tu sistema de facturación en PHP recibe un RUC escrito a mano y tiene que decidir en segundos si vale la pena emitir la factura. Necesitas la razón social exacta, saber si el contribuyente está ACTIVO y HABIDO, y tener la dirección fiscal para el comprobante. Entrar a la web de SUNAT cada vez funciona para tres clientes al día, no para trescientos.

En esta guía vas a consultar un RUC desde PHP de dos maneras: con curl nativo, que sirve para cualquier proyecto, y con el cliente HTTP de Laravel, que es más cómodo. Después vas a encapsular la consulta en un servicio reutilizable, validar el RUC dentro de un FormRequest y manejar el 404 y el resto de errores sin romper el flujo del usuario.

Los ejemplos usan la API de consulta RUC de endpoint.pe, pero la estructura (token Bearer, respuesta JSON, códigos HTTP) aplica a casi cualquier API REST que consumas desde PHP.


Lo que necesitas antes de empezar

Tres cosas: PHP 8.1 o superior con la extensión curl habilitada (viene activa en casi todas las instalaciones), un token de la API y un lugar seguro donde guardarlo. Si todavía no tienes token, crea una cuenta: incluye 150 consultas gratis al mes y no pide tarjeta.

El token nunca va escrito en el código. En PHP plano, guárdalo en una variable de entorno del servidor o en un archivo de configuración fuera del directorio público. En Laravel va en .env y se lee desde un archivo de config/. Más adelante verás el ejemplo; si quieres profundizar, revisa las buenas prácticas de seguridad de tokens.

La llamada que vas a replicar en PHP es esta:

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

Y la respuesta para el RUC de SUNAT, que usamos como ejemplo por ser una entidad pública, tiene esta forma (la dirección va recortada):

{
  "success": true,
  "message": "Consulta exitosa",
  "data": {
    "ruc": "20131312955",
    "razon_social": "SUPERINTENDENCIA NACIONAL DE ADUANAS Y DE ADMINISTRACION TRIBUTARIA - SUNAT",
    "estado": "ACTIVO",
    "condicion": "HABIDO",
    "direccion": "...",
    "ubigeo": "150101"
  },
  "meta": {
    "fuente": "SUNAT",
    "consultado_en": "2026-10-12T07:40:12Z",
    "creditos_cobrados": 1,
    "creditos_disponibles": 14621
  }
}

Guarda estos nombres de campo. Son los que vas a leer en PHP como claves de un arreglo asociativo.

Consulta con curl nativo en PHP

La extensión curl es la vía más directa y no requiere Composer. Esta función hace la petición, interpreta el código HTTP y devuelve un arreglo con los datos, o null cuando el RUC no existe en el padrón.

<?php

function consultarRuc(string $ruc): ?array
{
    $token = getenv('ENDPOINT_PE_TOKEN');
    $url = 'https://api.endpoint.pe/v1/ruc/' . urlencode($ruc);

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . $token,
            'Accept: application/json',
        ],
    ]);

    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $error = curl_error($ch);
    curl_close($ch);

    if ($body === false) {
        throw new RuntimeException("Error de conexión: $error");
    }

    if ($status === 404) {
        return null; // el RUC no está en el padrón
    }

    if ($status !== 200) {
        throw new RuntimeException("La API respondió con HTTP $status");
    }

    return json_decode($body, true, 512, JSON_THROW_ON_ERROR)['data'];
}

$empresa = consultarRuc('20131312955');

if ($empresa === null) {
    echo "RUC no encontrado\n";
} else {
    echo $empresa['razon_social'] . ' | ' . $empresa['estado'] . "\n";
}

Hay cuatro decisiones en ese código que conviene entender. CURLOPT_RETURNTRANSFER hace que curl_exec devuelva el cuerpo en lugar de imprimirlo. CURLOPT_TIMEOUT evita que una red lenta deje colgado tu proceso PHP: diez segundos es un valor razonable para una consulta puntual. El 404 se convierte en null porque "no existe" es una respuesta válida, no un fallo del sistema. Y JSON_THROW_ON_ERROR lanza una excepción si el cuerpo no es JSON válido, en vez de devolverte null en silencio.

Con esto ya tienes algo funcional para un script o un sistema sin framework. Si trabajas con Laravel, el siguiente camino es más corto.

El cliente HTTP de Laravel

Laravel incluye un cliente HTTP con una interfaz fluida (por debajo usa Guzzle). Primero declara el token y la URL base en config/services.php, que es el lugar habitual para credenciales de servicios externos:

// config/services.php
return [
    // ...
    'endpoint_pe' => [
        'token' => env('ENDPOINT_PE_TOKEN'),
        'base_url' => env('ENDPOINT_PE_URL', 'https://api.endpoint.pe/v1'),
    ],
];

Y en .env agrega ENDPOINT_PE_TOKEN=tu_token. Recuerda que env() solo debe usarse dentro de archivos de configuración; en el resto del código se lee con config(), que además funciona cuando la configuración está cacheada.

La consulta queda así:

use Illuminate\Support\Facades\Http;

$response = Http::withToken(config('services.endpoint_pe.token'))
    ->acceptJson()
    ->timeout(10)
    ->get(config('services.endpoint_pe.base_url') . '/ruc/20131312955');

if ($response->successful()) {
    $empresa = $response->json();
    // $empresa['razon_social'], $empresa['estado'], $empresa['condicion']
}

withToken() agrega la cabecera Authorization: Bearer por ti. successful() es verdadero para cualquier código 2xx y json() decodifica el cuerpo en un arreglo. Comparado con curl, son cinco líneas en lugar de treinta, y se leen de corrido.

Un servicio reutilizable

Si consultas RUC desde más de un lugar (registro de clientes, carga de proveedores, importación masiva), no repitas la llamada en cada controlador. Crea una clase de servicio con una sola responsabilidad: recibir un RUC y devolver los datos o null.

<?php

namespace App\Services;

use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use RuntimeException;

class RucService
{
    public function __construct(
        private readonly string $token,
        private readonly string $baseUrl,
    ) {}

    public function buscar(string $ruc): ?array
    {
        $clave = "ruc:$ruc";

        if ($enCache = Cache::get($clave)) {
            return $enCache;
        }

        $empresa = $this->consultar($ruc);

        if ($empresa !== null) {
            Cache::put($clave, $empresa, now()->addHours(24));
        }

        return $empresa;
    }

    private function consultar(string $ruc): ?array
    {
        try {
            $response = Http::withToken($this->token)
                ->acceptJson()
                ->timeout(10)
                ->get("$this->baseUrl/ruc/$ruc");
        } catch (ConnectionException $e) {
            throw new RuntimeException('No se pudo conectar con la API de RUC', previous: $e);
        }

        return match (true) {
            $response->status() === 404 => null,
            $response->status() === 401 => throw new RuntimeException('Token inválido o vencido'),
            $response->status() === 402 => throw new RuntimeException('Sin créditos disponibles'),
            $response->status() === 429 => throw new RuntimeException('Límite de velocidad superado'),
            $response->successful() => $response->json(),
            default => throw new RuntimeException('Respuesta inesperada: HTTP ' . $response->status()),
        };
    }
}

Regístralo en el contenedor para que Laravel lo inyecte donde lo pidas:

// app/Providers/AppServiceProvider.php
public function register(): void
{
    $this->app->singleton(RucService::class, fn () => new RucService(
        token: config('services.endpoint_pe.token'),
        baseUrl: config('services.endpoint_pe.base_url'),
    ));
}

A partir de ahí, cualquier controlador, comando de Artisan o job puede recibir RucService en su constructor o como parámetro del método y llamar a buscar().

Dos detalles de diseño. Primero, el servicio guarda en caché solo los resultados encontrados, durante 24 horas: el estado de un contribuyente cambia poco de un día a otro, y así ahorras créditos cuando el mismo RUC se consulta varias veces. Cuánto tiempo cachear cada dato lo explicamos en caché de consultas RUC y DNI. Segundo, la ConnectionException de Laravel (red caída, DNS, timeout) se relanza con un mensaje propio para que el resto de la aplicación no dependa de clases del cliente HTTP.

Validar el RUC en un FormRequest

El lugar natural para validar la entrada en Laravel es un FormRequest. Aquí conviene aplicar dos capas: primero el formato (11 dígitos, prefijo válido y dígito verificador), que no cuesta nada, y recién después la existencia en el padrón, que sí consume una consulta.

<?php

namespace App\Http\Requests;

use App\Services\RucService;
use Closure;
use Illuminate\Foundation\Http\FormRequest;

class GuardarClienteRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'ruc' => [
                'required',
                'digits:11',
                'regex:/^(10|15|17|20)\d{9}$/',
                function (string $attribute, mixed $value, Closure $fail) {
                    if (! $this->digitoVerificadorValido($value)) {
                        $fail('El RUC tiene un error de tipeo: el dígito verificador no coincide.');
                        return;
                    }

                    $empresa = app(RucService::class)->buscar($value);

                    if ($empresa === null) {
                        $fail('El RUC no existe en el padrón de SUNAT.');
                    }
                },
            ],
            'razon_social' => ['required', 'string', 'max:255'],
        ];
    }

    public function messages(): array
    {
        return [
            'ruc.digits' => 'El RUC debe tener exactamente 11 dígitos.',
            'ruc.regex' => 'El RUC debe empezar con 10, 15, 17 o 20.',
        ];
    }

    private function digitoVerificadorValido(string $ruc): bool
    {
        $pesos = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
        $suma = 0;

        foreach ($pesos as $i => $peso) {
            $suma += $peso * (int) $ruc[$i];
        }

        $resto = 11 - ($suma % 11);
        $digito = match ($resto) { 10 => 0, 11 => 1, default => $resto };

        return $digito === (int) $ruc[10];
    }
}

Las reglas se evalúan en orden, así que un RUC de 9 dígitos se rechaza en digits:11 y nunca llega a la API. El algoritmo del dígito verificador está explicado paso a paso en cómo validar un RUC; aquí solo lo aplicamos.

Un punto que suele pasarse por alto: si la API no responde (red caída, sin créditos), el servicio lanza una excepción dentro de la validación y el usuario vería un error 500. Decide qué prefieres. Puedes envolver la llamada en un try y fallar con un mensaje del tipo "No pudimos verificar el RUC, inténtalo de nuevo", o permitir el registro marcando el cliente como "pendiente de verificación" y revisarlo después con un job. Lo que no debes hacer es dejar que la excepción explote sin control.

Manejar el 404 y los demás errores

El 404 merece su propio tratamiento porque no es un fallo: significa que el RUC tiene formato correcto pero no figura en el padrón. Pasa con números mal copiados cuyo dígito verificador coincide por casualidad, o con RUC muy recientes. Tu sistema debe mostrarlo como un mensaje claro para el usuario, no como una pantalla de error. Además, las consultas sin resultado no se cobran, así que un 404 no gasta créditos.

Cuando el RUC sí existe, no te quedes con la razón social. Mira estado y condicion: un contribuyente con estado BAJA DEFINITIVA o condición NO HABIDO existe en el padrón, pero facturarle trae problemas. Si no tienes claro qué implica cada valor, lee qué significan ACTIVO y HABIDO antes de decidir cómo reacciona tu formulario.

Para el resto de códigos, esta tabla resume qué hacer:

CódigoSignificadoQué hacer en tu aplicación
401Token inválido o vencidoRevisar la variable de entorno; avisar al equipo técnico
402Sin créditosAvisar al administrador; no reintentar automáticamente
404RUC no encontradoMostrar mensaje al usuario; no es un error del sistema
429Límite de velocidadEsperar y reintentar con pausa creciente
5xxError del lado de la APIReintentar una o dos veces; luego registrar el fallo

Registra siempre el código y el RUC consultado en tus logs (nunca el token). Así, cuando un cliente reporte que "no le deja guardar un proveedor", vas a saber en un minuto si fue un 404 legítimo o un 402 por falta de créditos.

Para probar todo esto sin gastar consultas ni depender de la red, usa Http::fake() en tus tests de Laravel:

use Illuminate\Support\Facades\Http;

Http::fake([
    'api.endpoint.pe/v1/ruc/20131312955' => Http::response([
        'ruc' => '20131312955',
        'razon_social' => 'SUPERINTENDENCIA NACIONAL DE ADUANAS Y DE ADMINISTRACION TRIBUTARIA - SUNAT',
        'estado' => 'ACTIVO',
        'condicion' => 'HABIDO',
    ], 200),
    'api.endpoint.pe/v1/ruc/*' => Http::response([
        'error' => 'not_found',
        'mensaje' => 'No se encontró el RUC',
    ], 404),
]);

Con eso puedes escribir un test que envíe un RUC válido al FormRequest y espere éxito, y otro que envíe uno inexistente y espere el mensaje de "no existe en el padrón". Son dos tests baratos que te protegen de romper el registro de clientes en el próximo refactor.

En resumen

  • Con curl nativo bastan unas 30 líneas: cabecera Bearer, timeout, leer el código HTTP y decodificar el JSON.
  • En Laravel, Http::withToken() reduce la consulta a cinco líneas; guarda el token en .env y léelo con config().
  • Encapsula la llamada en un servicio con caché para no repetir código ni gastar créditos de más.
  • Valida formato y dígito verificador en el FormRequest antes de consultar la existencia.
  • Trata el 404 como "no existe", no como un fallo, y revisa estado y condición cuando sí existe.
  • Usa Http::fake() para probar sin red y sin consumir consultas.

Preguntas frecuentes

¿Puedo usar este código en CodeIgniter, Symfony u otro framework PHP?

Sí. La versión con curl nativo no depende de ningún framework. En Symfony puedes reemplazarla por su componente HttpClient con la misma lógica: cabecera Bearer, timeout y revisión del código de estado. El servicio y la validación en dos capas se trasladan sin cambios conceptuales.

¿Cuántas consultas gasto si valido el RUC en el FormRequest?

Una por RUC que pase el formato y no esté en caché. Un RUC con dígito verificador incorrecto se rechaza localmente y no consume nada. Un 404 tampoco se cobra. Con la caché de 24 horas del servicio, consultar el mismo RUC varias veces en el día cuesta un solo crédito.

¿Qué hago si la API no responde mientras el usuario guarda el formulario?

Captura la excepción del servicio dentro de la regla de validación y decide: rechazar con un mensaje de "inténtalo de nuevo" o aceptar el registro como pendiente de verificación. Para la segunda opción, un job en cola que reintente más tarde funciona muy bien y no bloquea al usuario.

¿Debo guardar la razón social que devuelve la API o la que escribe el usuario?

La de la API. Es la que figura en el padrón de SUNAT y la que debe ir en la factura electrónica. Puedes mostrársela al usuario para que confirme, pero el campo que se persiste debería ser el oficial, no el tipeado.

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.