Como consultar CNPJ em C# (.NET)
Este guia monta um cliente C# para a API de CNPJ da apipj usando só a biblioteca padrão do .NET: HttpClient, System.Text.Json e records tipados. Ele valida o dígito verificador antes de chamar a API (inclusive o CNPJ alfanumérico), trata "não encontrado" e limite por minuto, entra no ASP.NET Core com cache e consulta listas em paralelo. O código foi executado contra a API em 09/10/2026 com .NET 8.0.
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. - Guarde a chave fora do código. Em desenvolvimento, use o Secret Manager; em produção, uma variável de ambiente ou o cofre de segredos da sua nuvem:
dotnet user-secrets init
dotnet user-secrets set APIPJ_KEY "apipj_live_sua_chave"O cliente
Salve como ApipjCnpj.cs num projeto .NET 8 ou mais novo:
// Cliente mínimo da API de CNPJ da apipj (.NET 8, só a biblioteca padrão).
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.RegularExpressions;
public sealed class ApipjErro(int status, string codigo, string mensagem) : Exception($"{status} {codigo}: {mensagem}")
{
public int Status { get; } = status;
public string Codigo { get; } = codigo;
}
public sealed record Municipio(string? CodigoIbge, string? CodigoRfb, string? Nome);
public sealed record Endereco(string? TipoLogradouro, string? Logradouro, string? Numero, string? Complemento,
string? Bairro, string? Cep, Municipio Municipio, string? Uf);
public sealed record CodigoDescricao(string? Codigo, string? Descricao);
public sealed record Simples(bool Optante);
public sealed record Empresa(string Cnpj, string CnpjFormatado, string RazaoSocial, string? NomeFantasia,
CodigoDescricao SituacaoCadastral, CodigoDescricao CnaePrincipal,
Endereco Endereco, Simples Simples);
public static partial class Cnpj
{
static readonly int[] Pesos = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
[GeneratedRegex("[^0-9A-Za-z]")] private static partial Regex NaoAlfanumerico();
[GeneratedRegex("^[A-Z0-9]{12}[0-9]{2}$")] private static partial Regex Formato();
public static string Normalizar(string cnpj) => NaoAlfanumerico().Replace(cnpj, "").ToUpperInvariant();
/// <summary>Formato e dígitos verificadores, numérico ou alfanumérico (valor = código ASCII - 48).</summary>
public static bool Valido(string cnpj)
{
var c = Normalizar(cnpj);
if (!Formato().IsMatch(c)) return false;
var v = c.Select(ch => ch - 48).ToArray();
int Dv(int n)
{
var soma = 0;
for (var i = 0; i < n; i++) soma += v[i] * Pesos[i + 13 - n];
var resto = soma % 11;
return resto < 2 ? 0 : 11 - resto;
}
return v[12] == Dv(12) && v[13] == Dv(13);
}
}
public sealed class ApipjClient
{
static readonly JsonSerializerOptions Json = new() { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
readonly HttpClient _http;
// No ASP.NET Core, registre pela fábrica (AddHttpClient + AddTypedClient) em vez de criar um HttpClient por consulta.
public ApipjClient(HttpClient http, string chave, string baseUrl = "https://apipj.com.br")
{
_http = http;
_http.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/");
_http.Timeout = TimeSpan.FromSeconds(10);
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", chave);
}
/// <summary>Documento do CNPJ, ou null se ele não existe na base da Receita.</summary>
public async Task<Empresa?> ConsultarAsync(string cnpj, int tentativas = 3, CancellationToken ct = default)
{
var url = $"v1/cnpj/{Cnpj.Normalizar(cnpj)}";
for (var t = 1; t <= tentativas; t++)
{
using var r = await _http.GetAsync(url, ct);
if (r.StatusCode == HttpStatusCode.OK) return await r.Content.ReadFromJsonAsync<Empresa>(Json, ct);
if (r.StatusCode == HttpStatusCode.NotFound) return null;
var erro = await LerErroAsync(r, ct);
// 429 por minuto: espere o Retry-After e repita. Cota do mês esgotada (quota_exceeded) não adianta repetir.
if ((int)r.StatusCode == 429 && erro.Code == "rate_limited" && t < tentativas)
{
await Task.Delay(r.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(1), ct);
continue;
}
if ((int)r.StatusCode >= 500 && t < tentativas)
{
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, t)), ct);
continue;
}
throw new ApipjErro((int)r.StatusCode, erro.Code ?? "erro", erro.Message ?? r.ReasonPhrase ?? "");
}
throw new ApipjErro(429, "rate_limited", "limite por minuto atingido em todas as tentativas");
}
sealed record ErroApi(string? Code, string? Message);
static async Task<ErroApi> LerErroAsync(HttpResponseMessage r, CancellationToken ct)
{
try { return await r.Content.ReadFromJsonAsync<ErroApi>(Json, ct) ?? new ErroApi(null, null); }
catch (JsonException) { return new ErroApi(null, null); }
}
}O que esse código resolve:
- Records com
SnakeCaseLower. A API responde emsnake_case(razao_social) e a política de nomes do .NET 8 converte paraRazaoSocialsem atributos. Os records trazem só os campos usados aqui; campos a mais na resposta são ignorados, então acrescente os que precisar. - 404 vira
null. CNPJ inexistente, ou recém-aberto e ainda fora da carga do mês, é resultado, não exceção. Retry-Aftertipado.r.Headers.RetryAfter?.Deltajá chega comoTimeSpan. Só o 429rate_limited(limite por minuto) é repetido;quota_exceeded(cota do mês) lança na hora.- Um
HttpClientreaproveitado. Criar um por consulta esgota portas do sistema sob carga. Num console, crie um só; no ASP.NET Core, deixe a fábrica cuidar disso (veja abaixo).
Primeira consulta
using var http = new HttpClient();
var api = new ApipjClient(http, Environment.GetEnvironmentVariable("APIPJ_KEY")!);
var empresa = await api.ConsultarAsync("11.378.117/0001-20");
Console.WriteLine(empresa?.RazaoSocial); // LEADS2B S/A
Console.WriteLine(empresa?.SituacaoCadastral.Descricao); // ATIVA
Console.WriteLine(empresa?.Endereco.Municipio.CodigoIbge); // 4106902Tratando erros
try
{
await api.ConsultarAsync("11378117000121"); // dígito errado
}
catch (ApipjErro e) when (e.Codigo == "invalid_cnpj")
{
Console.WriteLine(e.Message); // 400 invalid_cnpj: Dígitos verificadores inválidos
}| HTTP | code | Comportamento |
|---|---|---|
| 200 | Devolve o Empresa. | |
| 400 | invalid_cnpj | Lança ApipjErro. Valide antes com Cnpj.Valido. |
| 401 | unauthorized | Lança ApipjErro. Confira a chave. |
| 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. |
No ASP.NET Core, com cache
Registre o cliente pela fábrica de HttpClient e guarde as respostas em memória. A base da Receita muda uma vez por mês, então 30 dias de cache é seguro para cadastro:
using Microsoft.Extensions.Caching.Memory;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMemoryCache();
builder.Services.AddHttpClient("apipj")
.AddTypedClient((http, sp) => new ApipjClient(http, sp.GetRequiredService<IConfiguration>()["APIPJ_KEY"]!));
var app = builder.Build();
app.MapGet("/clientes/cnpj/{cnpj}", async (string cnpj, ApipjClient apipj, IMemoryCache cache, CancellationToken ct) =>
{
if (!Cnpj.Valido(cnpj)) return Results.BadRequest(new { erro = "CNPJ inválido" });
var empresa = await cache.GetOrCreateAsync($"cnpj:{Cnpj.Normalizar(cnpj)}", item =>
{
item.AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(30); // a base da Receita muda uma vez por mês
return apipj.ConsultarAsync(cnpj, ct: ct);
});
return empresa is null ? Results.NotFound() : Results.Ok(empresa);
});
app.Run();Detalhes que costumam dar erro:
AddTypedClientcom fábrica. O construtor recebe a chave, que o contêiner de injeção não sabe resolver sozinho. A fábrica lêAPIPJ_KEYda configuração (variável de ambiente ou Secret Manager) e entrega umHttpClientgerenciado.- CNPJ sem máscara na rota. A barra de
11.378.117/0001-20quebra o caminho da URL. Peça só os caracteres ao front-end, ou receba o CNPJ por query string. - O cache também guarda o "não encontrado". Isso evita repetir consultas de CNPJ inexistente até a próxima carga mensal.
No teste, a primeira chamada do endpoint levou cerca de 240 ms e a repetição saiu do cache em cerca de 1 ms. Com várias instâncias, troque IMemoryCache por IDistributedCache com Redis.
Lista de CNPJs
using System.Collections.Concurrent;
var lista = new[] { "11378117000120", "00000000000191", "60746948000112", "invalido", "11.378.117/0001-20" };
var unicos = lista.Where(Cnpj.Valido).Select(Cnpj.Normalizar).Distinct().ToList();
var resultados = new ConcurrentDictionary<string, Empresa?>();
await Parallel.ForEachAsync(unicos, new ParallelOptions { MaxDegreeOfParallelism = 5 }, async (cnpj, ct) =>
{
resultados[cnpj] = await api.ConsultarAsync(cnpj, ct: ct);
});
foreach (var cnpj in unicos)
Console.WriteLine($"{cnpj} {resultados[cnpj]?.RazaoSocial ?? "não encontrado"}");O inválido e o duplicado saem antes de chamar a API: três consultas em vez de cinco. MaxDegreeOfParallelism limita quantas consultas rodam juntas, mas não aumenta o limite por minuto da chave; quando ele estoura, o cliente espera o Retry-After sozinho. Os planos pagos vão de 30 a 200 consultas por minuto (Preços).
Próximos passos
- Documentação completa: todos os 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, PHP.