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

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

  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. 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 em snake_case (razao_social) e a política de nomes do .NET 8 converte para RazaoSocial sem 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-After tipado. r.Headers.RetryAfter?.Delta já chega como TimeSpan. Só o 429 rate_limited (limite por minuto) é repetido; quota_exceeded (cota do mês) lança na hora.
  • Um HttpClient reaproveitado. 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);  // 4106902

Tratando 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
}
HTTPcodeComportamento
200Devolve o Empresa.
400invalid_cnpjLança ApipjErro. Valide antes com Cnpj.Valido.
401unauthorizedLança ApipjErro. Confira a chave.
404not_foundDevolve null.
429rate_limitedEspera o Retry-After e repete.
429quota_exceededLança ApipjErro.
5xxRepete 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:

  • AddTypedClient com fábrica. O construtor recebe a chave, que o contêiner de injeção não sabe resolver sozinho. A fábrica lê APIPJ_KEY da configuração (variável de ambiente ou Secret Manager) e entrega um HttpClient gerenciado.
  • CNPJ sem máscara na rota. A barra de 11.378.117/0001-20 quebra 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