Guias · atualizado em · 3 min de leitura · Equipe apipj

Como consultar CNPJ em PHP

Este guia monta funções PHP para a API de CNPJ da apipj usando só a extensão curl, que já vem na maioria das instalações, sem Composer. Elas validam o dígito verificador antes de chamar a API (inclusive o CNPJ alfanumérico), tratam "não encontrado" e limite por minuto, consultam listas, guardam em cache e convertem a resposta para o seu cadastro. O código foi executado contra a API em 09/10/2026 com PHP 8.3.

Antes de começar

  1. Crie uma conta grátis e copie a chave (apipj_live_...). O plano gratuito tem 10 consultas por minuto e 50.000 por mês.
  2. Coloque a chave numa variável de ambiente do servidor (no .env do Laravel ou do Symfony, na configuração do PHP-FPM ou do Apache), nunca num arquivo versionado. O código lê APIPJ_KEY com getenv.
  3. Confirme a extensão: php -m | grep curl deve mostrar curl.

As funções

Salve como apipj_cnpj.php (PHP 8.1 ou mais novo):

<?php
// Cliente mínimo da API de CNPJ da apipj (PHP 8.1+, extensão curl, sem dependências).

final class ApipjErro extends RuntimeException
{
    public function __construct(public readonly int $status, public readonly string $erro, string $mensagem)
    {
        parent::__construct("$status $erro: $mensagem");
    }
}

function normalizar_cnpj(string $cnpj): string
{
    return strtoupper(preg_replace('/[^0-9A-Za-z]/', '', $cnpj));
}

/** Formato e dígitos verificadores, numérico ou alfanumérico (valor = código ASCII - 48). */
function cnpj_valido(string $cnpj): bool
{
    $c = normalizar_cnpj($cnpj);
    if (!preg_match('/^[A-Z0-9]{12}[0-9]{2}$/', $c)) {
        return false;
    }
    $pesos = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
    $v = array_map(fn ($ch) => ord($ch) - 48, str_split($c));
    $dv = function (int $n) use ($v, $pesos): int {
        $soma = 0;
        for ($i = 0; $i < $n; $i++) {
            $soma += $v[$i] * $pesos[$i + 13 - $n];
        }
        $resto = $soma % 11;
        return $resto < 2 ? 0 : 11 - $resto;
    };
    return $v[12] === $dv(12) && $v[13] === $dv(13);
}

/** Documento do CNPJ, ou null se ele não existe na base da Receita. */
function consultar_cnpj(string $cnpj, string $formato = 'completo', int $tentativas = 3): ?array
{
    $base = rtrim(getenv('APIPJ_BASE_URL') ?: 'https://apipj.com.br', '/');
    $chave = getenv('APIPJ_KEY'); // nunca deixe a chave no código-fonte
    $url = "$base/v1/cnpj/" . normalizar_cnpj($cnpj) . '?formato=' . urlencode($formato);

    for ($t = 1; $t <= $tentativas; $t++) {
        $cabecalhos = [];
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => 10,
            CURLOPT_HTTPHEADER => ["Authorization: Bearer $chave", 'Accept: application/json'],
            CURLOPT_HEADERFUNCTION => function ($ch, string $linha) use (&$cabecalhos): int {
                $partes = explode(':', $linha, 2);
                if (count($partes) === 2) {
                    $cabecalhos[strtolower(trim($partes[0]))] = trim($partes[1]);
                }
                return strlen($linha);
            },
        ]);
        $corpo = curl_exec($ch);
        if ($corpo === false) {
            throw new ApipjErro(0, 'rede', curl_error($ch));
        }
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        $json = json_decode($corpo, true) ?? [];

        if ($status === 200) {
            return $json;
        }
        if ($status === 404) {
            return null;
        }
        $erro = $json['code'] ?? 'erro';
        // 429 por minuto: espere o Retry-After e repita. Cota do mês esgotada (quota_exceeded) não adianta repetir.
        if ($status === 429 && $erro === 'rate_limited' && $t < $tentativas) {
            sleep((int) ($cabecalhos['retry-after'] ?? 1));
            continue;
        }
        if ($status >= 500 && $t < $tentativas) {
            sleep(2 ** $t);
            continue;
        }
        throw new ApipjErro($status, $erro, $json['message'] ?? substr($corpo, 0, 200));
    }
    throw new ApipjErro(429, 'rate_limited', 'limite por minuto atingido em todas as tentativas');
}

Pontos de atenção:

  • CURLOPT_TIMEOUT. Sem ele, uma falha de rede segura o processo do PHP-FPM até o tempo máximo do servidor.
  • CURLOPT_HEADERFUNCTION captura o Retry-After, que o curl não expõe de outro jeito quando você pede só o corpo.
  • 404 vira null. CNPJ inexistente, ou recém-aberto e ainda fora da carga do mês, é resultado, não exceção.
  • rate_limited x quota_exceeded. Os dois chegam como HTTP 429. Só o primeiro (limite por minuto) vale repetir depois do Retry-After.

Primeira consulta

<?php
require __DIR__ . '/apipj_cnpj.php';

$empresa = consultar_cnpj('11.378.117/0001-20');
echo $empresa['razao_social'], PHP_EOL;                          // LEADS2B S/A
echo $empresa['situacao_cadastral']['descricao'], PHP_EOL;       // ATIVA
echo $empresa['endereco']['municipio']['codigo_ibge'], PHP_EOL;  // 4106902

$simples = consultar_cnpj('00000000000191', 'simplificado');
echo $simples['cnpj'], PHP_EOL;                                  // 00.000.000/0001-91

Tratando erros

try {
    consultar_cnpj('11378117000121'); // dígito errado
} catch (ApipjErro $e) {
    echo $e->status, ' ', $e->erro; // 400 invalid_cnpj
}
HTTPcodeComportamento
200Devolve o array do documento.
400invalid_cnpjLança ApipjErro. Valide antes com cnpj_valido.
401unauthorizedLança ApipjErro. Confira APIPJ_KEY.
404not_foundDevolve null.
429rate_limitedEspera o Retry-After e repete.
429quota_exceededLança ApipjErro.
5xxRepete com espera de 2 s e 4 s.

Cache em arquivo

A base da Receita muda uma vez por mês, então uma resposta pode ficar guardada por até 30 dias. Uma versão simples, em arquivo:

function consultar_cnpj_com_cache(string $cnpj, string $pasta = __DIR__ . '/cache-cnpj'): ?array
{
    $validade = 30 * 24 * 3600;
    if (!is_dir($pasta)) {
        mkdir($pasta, 0700, true);
    }
    $arquivo = "$pasta/" . normalizar_cnpj($cnpj) . '.json';
    if (is_file($arquivo) && time() - filemtime($arquivo) < $validade) {
        return json_decode(file_get_contents($arquivo), true);
    }
    $empresa = consultar_cnpj($cnpj);
    if ($empresa !== null) {
        file_put_contents($arquivo, json_encode($empresa, JSON_UNESCAPED_UNICODE));
    }
    return $empresa;
}

No teste, a segunda consulta do mesmo CNPJ saiu do cache em cerca de 6 ms. Em Laravel, o equivalente é Cache::remember('cnpj:'.$cnpj, now()->addDays(30), fn () => consultar_cnpj($cnpj)).

Lista de CNPJs

$lista = ['11378117000120', '00000000000191', '60746948000112', 'invalido', '11.378.117/0001-20'];
$unicos = array_unique(array_map('normalizar_cnpj', array_filter($lista, 'cnpj_valido')));
foreach ($unicos as $cnpj) {
    $empresa = consultar_cnpj_com_cache($cnpj);
    echo $cnpj, ' ', $empresa['razao_social'] ?? 'não encontrado', PHP_EOL;
}

O inválido e o duplicado (o mesmo CNPJ com máscara) saem antes de chamar a API: três consultas em vez de cinco. Para listas grandes, rode a importação numa fila (job) em vez de na requisição do usuário; quando o limite por minuto estourar, a função espera sozinha. Os planos pagos vão de 30 a 200 consultas por minuto (Preços).

Do JSON para o seu cadastro

function para_cadastro(array $e): array
{
    $end = $e['endereco'];
    return [
        'cnpj' => $e['cnpj_formatado'],
        'razao_social' => $e['razao_social'],
        'nome_fantasia' => $e['nome_fantasia'] ?? '',
        'situacao' => $e['situacao_cadastral']['descricao'],
        'logradouro' => trim(($end['tipo_logradouro'] ?? '') . ' ' . ($end['logradouro'] ?? '')),
        'numero' => $end['numero'] ?? 'S/N',
        'bairro' => $end['bairro'] ?? '',
        'cep' => $end['cep'],
        'cidade' => $end['municipio']['nome'],
        'uf' => $end['uf'],
        'codigo_ibge' => $end['municipio']['codigo_ibge'],
        'optante_simples' => (bool) ($e['simples']['optante'] ?? false),
    ];
}

Resultado real para os Correios (34.028.316/0001-03): SETOR SBN QUADRA 1 BLOCO A, ASA NORTE, 70002900, BRASILIA/DF, IBGE 5300108. Antes de emitir nota ou aprovar um pedido, confira situacao: empresa que não está ATIVA costuma gerar rejeição.

Próximos passos