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
- 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. - Coloque a chave numa variável de ambiente do servidor (no
.envdo Laravel ou do Symfony, na configuração do PHP-FPM ou do Apache), nunca num arquivo versionado. O código lêAPIPJ_KEYcomgetenv. - Confirme a extensão:
php -m | grep curldeve mostrarcurl.
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_HEADERFUNCTIONcaptura oRetry-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_limitedxquota_exceeded. Os dois chegam como HTTP 429. Só o primeiro (limite por minuto) vale repetir depois doRetry-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-91Tratando erros
try {
consultar_cnpj('11378117000121'); // dígito errado
} catch (ApipjErro $e) {
echo $e->status, ' ', $e->erro; // 400 invalid_cnpj
}| HTTP | code | Comportamento |
|---|---|---|
| 200 | Devolve o array do documento. | |
| 400 | invalid_cnpj | Lança ApipjErro. Valide antes com cnpj_valido. |
| 401 | unauthorized | Lança ApipjErro. Confira APIPJ_KEY. |
| 404 | not_found | Devolve null. |
| 429 | rate_limited | Espera o Retry-After e repete. |
| 429 | quota_exceeded | Lança ApipjErro. |
| 5xx | Repete 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
- Documentação completa: campos, formatos e cabeçalhos de limite.
- CNPJ alfanumérico: regras da Receita e o cálculo do dígito.
- Em outra linguagem: Python, JavaScript, C#.