# Documentação da API de CNPJ

Referência completa da API do apipj. Base: `https://apipj.com.br/v1`. Todas as respostas são JSON em UTF-8. Esta página também existe em [Markdown](https://apipj.com.br/docs.md) e há um [OpenAPI 3.1](https://apipj.com.br/openapi.json) gerado a partir das rotas. Para testar sem escrever código, abra o [sandbox](https://apipj.com.br/sandbox).

## Visão geral

- **Fonte dos dados:** arquivos de dados abertos da Receita Federal do Brasil, carregados em lote uma vez por mês. Cada documento traz o campo `atualizado_em` com o mês do lote (`AAAA-MM`). Não há consulta em tempo real.
- **Autenticação:** obrigatória em `GET /v1/cnpj` (chave de API; sem chave a resposta é 401 `unauthorized`). A conta é grátis e não pede cartão: 10 por minuto e 50.000 por mês. `/v1/valida` e `/v1/status` são abertos.
- **Formatos:** `completo` (padrão: objetos aninhados, datas ISO `AAAA-MM-DD` e códigos da Receita ao lado das descrições) e `simplificado` (formato plano com campos já formatados: datas DD/MM/AAAA, CNPJ e CEP com pontuação), para integrações que preferem receber os valores prontos para exibir.
- **CNPJ alfanumérico:** aceito em todos os endpoints desde o primeiro dia. Entrada com ou sem pontuação, maiúsculas ou minúsculas.
- **CORS:** liberado (qualquer origem, sem credenciais) em `GET /v1/cnpj/*`, `/v1/valida/*`, `/v1/status`, `/v1/plans` e `/openapi.json`; você pode chamar direto do navegador. Os cabeçalhos de limite e o `Retry-After` ficam visíveis para o JavaScript. Nas demais rotas o navegador bloqueia chamadas vindas de outra origem.
- **Compressão:** gzip e brotli, negociadas pelo header `Accept-Encoding`.

## Autenticação

Crie uma conta em [/criar-conta](https://apipj.com.br/criar-conta): a primeira chave é gerada na hora e exibida uma única vez. Você pode ter até 5 chaves ativas por conta e revogar qualquer uma no [painel](https://apipj.com.br/painel); a revogação é imediata.

Envie a chave de uma destas formas, em ordem de preferência:

| Forma | Exemplo | Observação |
|---|---|---|
| Header `Authorization` | `Authorization: Bearer apipj_live_...` | Recomendado |
| Header `X-API-Key` | `X-API-Key: apipj_live_...` | Alternativa para clientes que não montam `Authorization` |
| Query string `?token=` | `/v1/cnpj/...?token=apipj_live_...` | Só para clientes que não conseguem enviar cabeçalhos. **Não recomendado**: a chave vai parar em logs e históricos |

No próprio site (sandbox e consulta da página inicial), quem está logado consulta pela sessão (cookie, mesma origem), sem chave; o uso conta na mesma cota mensal da conta. Sem chave e sem sessão, `GET /v1/cnpj` responde `401` com `{"status":"ERROR","message":"Crie uma conta grátis para consultar","code":"unauthorized"}`.

Chaves têm o prefixo `apipj_live_`. Guardamos apenas um hash; se você perder a chave, revogue e crie outra. Nunca exponha a chave em código público, em apps distribuídos ou no front-end de sites abertos: use um backend intermediário.

## Limites e cotas

Dois limites agem ao mesmo tempo: um **por minuto** (token bucket; zera a cada 60 segundos) e uma **cota mensal** por conta (somando todas as chaves e o uso pela sessão do site). Os valores vêm de [/precos](https://apipj.com.br/precos):

| Plano | Por minuto | Cota |
|---|---|---|
| Gratuito | 10 | 50.000 por mês |
| Dev (R$ 12,90) | 30 | 200.000 por mês |
| Pro (R$ 59,90) | 200 | 600.000 por mês |

As respostas de `/v1/cnpj` que passam pela autenticação trazem os cabeçalhos abaixo, inclusive 404 e 429. Os erros que saem antes da contagem (400, 401 e 414) vêm sem eles, e o 503 traz só os três `X-RateLimit-*`:

| Cabeçalho | Significado |
|---|---|
| `X-RateLimit-Limit` | Consultas permitidas por minuto (por chave; no site, por conta) |
| `X-RateLimit-Remaining` | Quantas ainda cabem neste minuto |
| `X-RateLimit-Reset` | Segundos até o limite por minuto zerar |
| `X-Quota-Limit` | Cota mensal da conta |
| `X-Quota-Remaining` | Quanto resta da cota |
| `X-Quota-Reset` | Instante em que a cota zera (epoch Unix em segundos; início do próximo mês, horário de Brasília). Ex.: `1793502000` = 01/11/2026 00:00 em Brasília |
| `Retry-After` | Só em 429: segundos a esperar antes de tentar de novo |

A cota mensal reinicia no primeiro dia de cada mês (horário de Brasília) e não acumula. `/v1/valida` e `/v1/status` não consomem cota.

## GET /v1/cnpj/{cnpj}

Devolve o documento cadastral de um CNPJ. Parâmetro de query `formato`: `completo` (padrão, pode ser omitido) ou `simplificado`. Qualquer outro valor responde 400 `validation_error`.

::: tabs
```bash curl
curl -s "https://apipj.com.br/v1/cnpj/11378117000120" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer apipj_live_SUA_CHAVE"
```
```js JavaScript
const resp = await fetch("https://apipj.com.br/v1/cnpj/11378117000120", {
  headers: {
    Accept: "application/json",
    Authorization: "Bearer apipj_live_SUA_CHAVE",
  },
});
if (!resp.ok) {
  const erro = await resp.json().catch(() => ({}));
  throw new Error(`HTTP ${resp.status}: ${erro.message ?? "erro"}`);
}
const dados = await resp.json();
console.log(dados.razao_social, dados.situacao_cadastral.descricao);
console.log("restam no minuto:", resp.headers.get("X-RateLimit-Remaining"));
```
```python Python
import requests

r = requests.get(
    "https://apipj.com.br/v1/cnpj/11378117000120",
    headers={
        "Accept": "application/json",
        "Authorization": "Bearer apipj_live_SUA_CHAVE",
    },
    timeout=10,
)
if r.status_code == 429:
    raise SystemExit(f"limite: tente em {r.headers.get('Retry-After')} s")
r.raise_for_status()
dados = r.json()
print(dados["razao_social"], dados["situacao_cadastral"]["descricao"])
```
```php PHP
<?php
$ch = curl_init("https://apipj.com.br/v1/cnpj/11378117000120");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 10,
    CURLOPT_HTTPHEADER     => [
        "Accept: application/json",
        "Authorization: Bearer apipj_live_SUA_CHAVE",
    ],
]);
$corpo  = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 200) {
    throw new RuntimeException("HTTP $status: $corpo");
}
$dados = json_decode($corpo, true, 512, JSON_THROW_ON_ERROR);
echo $dados["razao_social"], " - ", $dados["situacao_cadastral"]["descricao"], PHP_EOL;
```
```csharp C#
using System.Net.Http.Headers;
using System.Text.Json;

using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "apipj_live_SUA_CHAVE");

using var resp = await http.GetAsync("https://apipj.com.br/v1/cnpj/11378117000120");
if (!resp.IsSuccessStatusCode)
    throw new HttpRequestException($"HTTP {(int)resp.StatusCode}: {await resp.Content.ReadAsStringAsync()}");

using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
var raiz = doc.RootElement;
Console.WriteLine(raiz.GetProperty("razao_social"));
```
:::

### Resposta 200 (formato completo)

```json
{
  "cnpj": "11378117000120",
  "cnpj_formatado": "11.378.117/0001-20",
  "razao_social": "LEADS2B S/A",
  "nome_fantasia": "LEADS2B.COM",
  "matriz_filial": "MATRIZ",
  "situacao_cadastral": {
    "codigo": "02",
    "descricao": "ATIVA",
    "data": "2009-11-24",
    "motivo": {
      "codigo": "00",
      "descricao": "SEM MOTIVO"
    }
  },
  "situacao_especial": {
    "descricao": null,
    "data": null
  },
  "data_inicio_atividade": "2009-11-24",
  "natureza_juridica": {
    "codigo": "2054",
    "descricao": "Sociedade Anônima Fechada"
  },
  "qualificacao_responsavel": {
    "codigo": "10",
    "descricao": "Diretor"
  },
  "porte": {
    "codigo": "05",
    "descricao": "DEMAIS"
  },
  "capital_social": 218878,
  "cnae_principal": {
    "codigo": "6311900",
    "descricao": "Tratamento de dados, provedores de serviços de aplicação e serviços de hospedagem na internet"
  },
  "cnaes_secundarios": [
    {
      "codigo": "8599604",
      "descricao": "Treinamento em desenvolvimento profissional e gerencial"
    }
  ],
  "endereco": {
    "tipo_logradouro": "RUA",
    "logradouro": "PADRE ANCHIETA",
    "numero": "2285",
    "complemento": "CONJ 1603 ANDAR 16 COND BUSINESS CENTER",
    "bairro": "BIGORRILHO",
    "cep": "80730001",
    "municipio": {
      "codigo_rfb": "7535",
      "codigo_ibge": "4106902",
      "nome": "CURITIBA"
    },
    "uf": "PR",
    "pais": null,
    "cidade_exterior": null
  },
  "telefones": [
    {
      "ddd": "41",
      "numero": "30287828"
    }
  ],
  "fax": null,
  "email": "contabilidade@leads2b.com",
  "ente_federativo_responsavel": null,
  "simples": {
    "optante": false,
    "data_opcao": "2009-11-24",
    "data_exclusao": "2019-12-31"
  },
  "mei": {
    "optante": false,
    "data_opcao": null,
    "data_exclusao": null
  },
  "socios": [
    {
      "nome": "FULANO DE TAL",
      "tipo": "PESSOA FISICA",
      "cpf_cnpj": "***123456**",
      "qualificacao": {
        "codigo": "10",
        "descricao": "Diretor"
      },
      "data_entrada": "2009-11-24",
      "faixa_etaria": "41 a 50 anos",
      "pais": null,
      "representante_legal": null
    }
  ],
  "atualizado_em": "2026-09"
}
```

Regras do documento:

- Datas sempre em ISO `AAAA-MM-DD` ou `null`. Nunca inventamos campos: o que a Receita não traz vem como `null`.
- `cnpj` tem 14 caracteres sem pontuação, em maiúsculas; `cnpj_formatado` tem a máscara `00.000.000/0000-00`.
- `situacao_cadastral.descricao` é uma de `ATIVA`, `BAIXADA`, `INAPTA`, `SUSPENSA`, `NULA`.
- `capital_social` é número (reais, duas casas).
- `endereco.municipio.codigo_ibge` é o código IBGE de 7 dígitos (o que a NFS-e pede) e `endereco.municipio.codigo_rfb` é o código de 4 dígitos da própria Receita. Os 5.571 municípios têm os dois; só estabelecimentos no exterior (`codigo_rfb` `9707`) vêm com `codigo_ibge` `null`.
- `qualificacao_responsavel` (`{codigo, descricao}`) é a qualificação, na Receita, da pessoa responsável pela empresa (ex.: `10`, `Diretor`).
- `endereco.cidade_exterior` só vem preenchida para estabelecimento no exterior; nos demais casos é `null`.
- `socios[].cpf_cnpj` vem exatamente como a Receita publica: CPF mascarado (`***123456**`) para pessoa física e, para sócio pessoa jurídica, só a raiz de 8 caracteres do CNPJ (não completamos a ordem nem os dígitos verificadores). Esses são dados pessoais de acesso público; veja a seção sobre LGPD na [política de privacidade](https://apipj.com.br/privacidade).
- `socios[].representante_legal` é `null` ou `{cpf, nome, qualificacao: {codigo, descricao}}`, com o CPF como a Receita publica.
- `atualizado_em` é o mês do lote de dados abertos em uso.

### Erros

| HTTP | `code` | Quando | Corpo |
|---|---|---|---|
| 400 | `invalid_cnpj` | CNPJ com tamanho, caractere ou dígito verificador inválido. É checado antes da chave: vem mesmo sem autenticação e não consome cota. A `message` é o mesmo `erro` de `/v1/valida` (ex.: `"Dígitos verificadores inválidos"`, `"CNPJ deve ter 14 caracteres (recebido 15)"`) e `cnpj` traz a entrada normalizada | `{"status":"ERROR","message":"Dígitos verificadores inválidos","code":"invalid_cnpj","cnpj":"11378117000121"}` |
| 400 | `validation_error` | Parâmetro fora do esperado: `formato` diferente de `completo` ou `simplificado`, parâmetro de query desconhecido ou CNPJ com mais de 32 caracteres | `{"status":"ERROR","message":"Dados inválidos: cnpj deve ter no máximo 32 caracteres","code":"validation_error","details":[...]}` |
| 401 | `unauthorized` | Sem chave e sem sessão do site (`"Crie uma conta grátis para consultar"`); chave sem o prefixo `apipj_live_` (`"Chave de API inválida"`); chave desconhecida ou revogada (`"Chave de API inválida ou revogada"`) | `{"status":"ERROR","message":"Chave de API inválida ou revogada","code":"unauthorized"}` |
| 404 | `not_found` | CNPJ válido, mas ausente no lote atual (conta na cota) | `{"status":"ERROR","message":"CNPJ não encontrado","code":"not_found"}` |
| 414 | `uri_too_long` | Parâmetro da URL com mais de 64 caracteres | `{"status":"ERROR","message":"Parâmetro da URL longo demais","code":"uri_too_long"}` |
| 429 | `rate_limited` | Limite por minuto excedido | `{"status":"ERROR","message":"Limite de consultas por minuto excedido","code":"rate_limited","retry_after":37}` + header `Retry-After` |
| 429 | `quota_exceeded` | Cota mensal esgotada. `reset` é o mesmo instante de `X-Quota-Reset` (epoch Unix) | `{"status":"ERROR","message":"Cota mensal esgotada","code":"quota_exceeded","upgrade_url":"https://apipj.com.br/precos","reset":1793502000}` |
| 503 | `dataset_unavailable` | Base de CNPJ não carregada (`/v1/status` mostra `DEGRADED`); tente de novo em instantes | `{"status":"ERROR","message":"Base de CNPJ indisponível no momento","code":"dataset_unavailable"}` |
| 5xx | `internal_error` | Falha do nosso lado | `{"status":"ERROR","message":"Erro interno","code":"internal_error"}`; confira [/status](https://apipj.com.br/status) |

Todo erro tem a forma `{status:"ERROR", message, code}`, às vezes com campos extras (`cnpj`, `retry_after`, `upgrade_url`, `reset`, `details`). `code` é o campo estável, em snake_case: trate os erros por ele. `message` é texto em português para humanos; os exemplos acima são ilustrativos e o texto pode mudar. `upgrade_url` aponta para a página de preços no endereço público do serviço.

## Formato simplificado

Acrescente `?formato=simplificado` para receber um formato plano, com os campos já formatados para exibir, nesta ordem: `abertura` (DD/MM/AAAA), `situacao`, `tipo`, `nome`, `fantasia`, `porte`, `natureza_juridica` (`"205-4 - Sociedade Anônima Fechada"`), `atividade_principal` (`[{code, text}]`, CNAE com máscara `"63.11-9-00"`), `atividades_secundarias`, `qsa` (sócios: `[{nome, qual, pais_origem, nome_rep_legal, qual_rep_legal}]`), `logradouro` (já com o tipo: `"RUA PADRE ANCHIETA"`), `numero`, `complemento`, `municipio`, `bairro`, `uf`, `cep` (`"80.730-001"`), `email`, `telefone` (`"(41) 3028-7828"`; havendo mais de um, separados por `" / "`), `data_situacao`, `cnpj` (com máscara), `ultima_atualizacao`, `status`, `efr`, `motivo_situacao`, `situacao_especial`, `data_situacao_especial`, `capital_social` (string com duas casas), `simples`, `simei`, `extra` e `billing`.

`ultima_atualizacao` (e o campo de mesmo nome dentro de `simples` e `simei`) é a data e hora, em ISO 8601 UTC, em que a base do mês foi gerada a partir dos arquivos da Receita; não é o mês dos dados (esse está no `atualizado_em` do formato completo e no `dataset_month` de `/v1/status`).

Diferenças em relação ao completo: textos vazios vêm como `""` em vez de `null`, códigos e descrições aparecem juntos em um só campo (`natureza_juridica`, `qsa[].qual`) e endereço e telefones vêm como texto em campos planos (o completo os detalha em objetos). `billing.database` vem sempre `true`, porque toda resposta sai da base mensal, e `billing.free` indica se a consulta foi feita no plano gratuito. Os limites, a cota e os erros são os mesmos nos dois formatos.

::: tabs
```bash curl
curl -s "https://apipj.com.br/v1/cnpj/11378117000120?formato=simplificado" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer apipj_live_SUA_CHAVE"
```
```js JavaScript
const resp = await fetch("https://apipj.com.br/v1/cnpj/11378117000120?formato=simplificado", {
  headers: {
    Accept: "application/json",
    Authorization: "Bearer apipj_live_SUA_CHAVE",
  },
});
if (!resp.ok) {
  const erro = await resp.json().catch(() => ({}));
  throw new Error(`HTTP ${resp.status}: ${erro.message ?? "erro"}`);
}
const dados = await resp.json();
console.log(dados.nome, dados.situacao);
console.log("restam no minuto:", resp.headers.get("X-RateLimit-Remaining"));
```
```python Python
import requests

r = requests.get(
    "https://apipj.com.br/v1/cnpj/11378117000120?formato=simplificado",
    headers={
        "Accept": "application/json",
        "Authorization": "Bearer apipj_live_SUA_CHAVE",
    },
    timeout=10,
)
if r.status_code == 429:
    raise SystemExit(f"limite: tente em {r.headers.get('Retry-After')} s")
r.raise_for_status()
dados = r.json()
print(dados["nome"], dados["situacao"])
```
```php PHP
<?php
$ch = curl_init("https://apipj.com.br/v1/cnpj/11378117000120?formato=simplificado");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 10,
    CURLOPT_HTTPHEADER     => [
        "Accept: application/json",
        "Authorization: Bearer apipj_live_SUA_CHAVE",
    ],
]);
$corpo  = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 200) {
    throw new RuntimeException("HTTP $status: $corpo");
}
$dados = json_decode($corpo, true, 512, JSON_THROW_ON_ERROR);
echo $dados["nome"], " - ", $dados["situacao"], PHP_EOL;
```
```csharp C#
using System.Net.Http.Headers;
using System.Text.Json;

using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "apipj_live_SUA_CHAVE");

using var resp = await http.GetAsync("https://apipj.com.br/v1/cnpj/11378117000120?formato=simplificado");
if (!resp.IsSuccessStatusCode)
    throw new HttpRequestException($"HTTP {(int)resp.StatusCode}: {await resp.Content.ReadAsStringAsync()}");

using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
var raiz = doc.RootElement;
Console.WriteLine(raiz.GetProperty("nome"));
```
:::

### Resposta 200 (formato simplificado)

```json
{
  "abertura": "24/11/2009",
  "situacao": "ATIVA",
  "tipo": "MATRIZ",
  "nome": "LEADS2B S/A",
  "fantasia": "LEADS2B.COM",
  "porte": "DEMAIS",
  "natureza_juridica": "205-4 - Sociedade Anônima Fechada",
  "atividade_principal": [
    {
      "code": "63.11-9-00",
      "text": "Tratamento de dados, provedores de serviços de aplicação e serviços de hospedagem na internet"
    }
  ],
  "atividades_secundarias": [
    {
      "code": "85.99-6-04",
      "text": "Treinamento em desenvolvimento profissional e gerencial"
    }
  ],
  "qsa": [
    {
      "nome": "FULANO DE TAL",
      "qual": "10-Diretor",
      "pais_origem": "",
      "nome_rep_legal": "",
      "qual_rep_legal": ""
    }
  ],
  "logradouro": "RUA PADRE ANCHIETA",
  "numero": "2285",
  "complemento": "CONJ 1603 ANDAR 16 COND BUSINESS CENTER",
  "municipio": "CURITIBA",
  "bairro": "BIGORRILHO",
  "uf": "PR",
  "cep": "80.730-001",
  "email": "contabilidade@leads2b.com",
  "telefone": "(41) 3028-7828",
  "data_situacao": "24/11/2009",
  "cnpj": "11.378.117/0001-20",
  "ultima_atualizacao": "2026-10-01T00:00:00.000Z",
  "status": "OK",
  "efr": "",
  "motivo_situacao": "",
  "situacao_especial": "",
  "data_situacao_especial": "",
  "capital_social": "218878.00",
  "simples": {
    "optante": false,
    "data_opcao": "24/11/2009",
    "data_exclusao": "31/12/2019",
    "ultima_atualizacao": "2026-10-01T00:00:00.000Z"
  },
  "simei": {
    "optante": false,
    "data_opcao": null,
    "data_exclusao": null,
    "ultima_atualizacao": "2026-10-01T00:00:00.000Z"
  },
  "extra": {},
  "billing": {
    "free": true,
    "database": true
  }
}
```

## GET /v1/valida/{cnpj}

Valida formato e dígito verificador, inclusive de CNPJ alfanumérico. Não exige chave, não consome cota e não consulta a base (um CNPJ pode ser válido e não existir).

::: tabs
```bash curl
curl -s "https://apipj.com.br/v1/valida/12ABC34501DE35" \
  -H "Accept: application/json"
# sem chave e sem cota (rota aberta)
```
```js JavaScript
const resp = await fetch("https://apipj.com.br/v1/valida/12ABC34501DE35", {
  headers: {
    Accept: "application/json",
    // Authorization: "Bearer apipj_live_...", // opcional nesta rota
  },
});
if (!resp.ok) {
  const erro = await resp.json().catch(() => ({}));
  throw new Error(`HTTP ${resp.status}: ${erro.message ?? "erro"}`);
}
const dados = await resp.json();
console.log(dados.valido, dados.formatado);
console.log("restam no minuto:", resp.headers.get("X-RateLimit-Remaining"));
```
```python Python
import requests

r = requests.get(
    "https://apipj.com.br/v1/valida/12ABC34501DE35",
    headers={
        "Accept": "application/json",
        # "Authorization": "Bearer apipj_live_...",  # opcional nesta rota
    },
    timeout=10,
)
if r.status_code == 429:
    raise SystemExit(f"limite: tente em {r.headers.get('Retry-After')} s")
r.raise_for_status()
dados = r.json()
print(dados["valido"], dados["formatado"])
```
```php PHP
<?php
$ch = curl_init("https://apipj.com.br/v1/valida/12ABC34501DE35");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 10,
    CURLOPT_HTTPHEADER     => [
        "Accept: application/json",
        // "Authorization: Bearer apipj_live_...", // opcional
    ],
]);
$corpo  = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 200) {
    throw new RuntimeException("HTTP $status: $corpo");
}
$dados = json_decode($corpo, true, 512, JSON_THROW_ON_ERROR);
echo var_export($dados["valido"], true), " - ", $dados["formatado"], PHP_EOL;
```
```csharp C#
using System.Net.Http.Headers;
using System.Text.Json;

using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "apipj_live_..."); // opcional

using var resp = await http.GetAsync("https://apipj.com.br/v1/valida/12ABC34501DE35");
if (!resp.IsSuccessStatusCode)
    throw new HttpRequestException($"HTTP {(int)resp.StatusCode}: {await resp.Content.ReadAsStringAsync()}");

using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
var raiz = doc.RootElement;
Console.WriteLine(raiz.GetProperty("valido"));
```
:::

### Resposta 200

```json
{
  "cnpj": "12ABC34501DE35",
  "valido": true,
  "formatado": "12.ABC.345/01DE-35",
  "alfanumerico": true,
  "erro": null
}
```

Com tamanho errado, caractere inválido ou DV errado a resposta continua 200, com `valido: false` e `erro` explicando (ex.: `"Dígitos verificadores inválidos"`, `"CNPJ deve ter 14 caracteres (recebido 15)"`, `"CNPJ zerado é inválido"`). As exceções: parâmetro acima de 32 caracteres, 400 `validation_error`; acima de 64, 414 `uri_too_long`; sem parâmetro (`/v1/valida/`), 404 `not_found`.

## GET /v1/status

Estado da API e do dataset. Sem chave, sem cota. Use para monitorar e para saber o mês dos dados antes de uma carga em lote.

::: tabs
```bash curl
curl -s "https://apipj.com.br/v1/status" \
  -H "Accept: application/json"
# sem chave e sem cota (rota aberta)
```
```js JavaScript
const resp = await fetch("https://apipj.com.br/v1/status", {
  headers: {
    Accept: "application/json",
    // Authorization: "Bearer apipj_live_...", // opcional nesta rota
  },
});
if (!resp.ok) {
  const erro = await resp.json().catch(() => ({}));
  throw new Error(`HTTP ${resp.status}: ${erro.message ?? "erro"}`);
}
const dados = await resp.json();
console.log(dados.status, dados.dataset_month);
console.log("restam no minuto:", resp.headers.get("X-RateLimit-Remaining"));
```
```python Python
import requests

r = requests.get(
    "https://apipj.com.br/v1/status",
    headers={
        "Accept": "application/json",
        # "Authorization": "Bearer apipj_live_...",  # opcional nesta rota
    },
    timeout=10,
)
if r.status_code == 429:
    raise SystemExit(f"limite: tente em {r.headers.get('Retry-After')} s")
r.raise_for_status()
dados = r.json()
print(dados["status"], dados["dataset_month"])
```
```php PHP
<?php
$ch = curl_init("https://apipj.com.br/v1/status");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 10,
    CURLOPT_HTTPHEADER     => [
        "Accept: application/json",
        // "Authorization: Bearer apipj_live_...", // opcional
    ],
]);
$corpo  = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 200) {
    throw new RuntimeException("HTTP $status: $corpo");
}
$dados = json_decode($corpo, true, 512, JSON_THROW_ON_ERROR);
echo $dados["status"], " - ", $dados["dataset_month"], PHP_EOL;
```
```csharp C#
using System.Net.Http.Headers;
using System.Text.Json;

using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
// http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "apipj_live_..."); // opcional

using var resp = await http.GetAsync("https://apipj.com.br/v1/status");
if (!resp.IsSuccessStatusCode)
    throw new HttpRequestException($"HTTP {(int)resp.StatusCode}: {await resp.Content.ReadAsStringAsync()}");

using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
var raiz = doc.RootElement;
Console.WriteLine(raiz.GetProperty("status"));
```
:::

### Resposta 200

```json
{
  "status": "OK",
  "dataset_month": "2026-09",
  "dataset_generated_at": "2026-10-01T03:12:45.000Z",
  "rows": 65000000,
  "version": "1.0.0",
  "time": "2026-10-09T12:00:00.000Z"
}
```

`status` é `OK` ou `DEGRADED`, com HTTP 200 nos dois. `DEGRADED` quer dizer API no ar, mas base de CNPJ não carregada: `/v1/cnpj` responde 503 `dataset_unavailable` e `dataset_month`, `dataset_generated_at` e `rows` vêm `null`. `dataset_month` é o mês dos dados da Receita (`AAAA-MM`, o mesmo `atualizado_em` dos documentos) e `dataset_generated_at` é a data e hora (UTC) em que a base desse mês foi gerada. Num monitor, cheque o campo `status`, não só o código HTTP.

## CNPJ alfanumérico

Desde 31/07/2026 a Receita Federal emite CNPJs com letras nas 12 primeiras posições (raiz de 8 e ordem de 4); os dois dígitos verificadores continuam numéricos. O formato é `[A-Z0-9]{12}[0-9]{2}`. O cálculo do DV usa módulo 11 com os pesos de sempre, mas o valor de cada caractere é o seu **código ASCII menos 48** (`0`→0 … `9`→9, `A`→17 … `Z`→42).

Exemplo válido para testes: `12.ABC.345/01DE-35`. A API aceita letras minúsculas e qualquer pontuação na entrada e normaliza antes de validar. Nos nossos documentos o campo `cnpj` vem sempre em maiúsculas sem pontuação, e `cnpj_formatado` com a máscara tradicional.

Se o seu sistema valida CNPJ localmente, troque a rotina pelo novo algoritmo ou use `/v1/valida` (grátis, sem cota) enquanto atualiza o seu sistema.

## Boas práticas

- **Cache por CNPJ.** A base muda uma vez por mês; guarde a resposta por até 30 dias (ou até o `dataset_month` de `/v1/status` passar do `atualizado_em` que você guardou). Isso reduz seu consumo de cota e a latência para zero na maioria das consultas repetidas.
- **Respeite `Retry-After`.** Em 429, espere o número de segundos indicado antes de tentar de novo; não faça retry imediato. Para 5xx, use backoff exponencial com jitter (1 s, 2 s, 4 s) e no máximo 3 tentativas.
- **Leia `X-RateLimit-Remaining` e `X-Quota-Remaining`.** Em cargas em lote, controle o ritmo pelo `X-RateLimit-Limit` em vez de disparar tudo e esperar o 429.
- **Valide antes de consultar.** `/v1/valida` (ou o algoritmo local) evita gastar cota com CNPJs digitados errado.
- **Trate `null`.** Muitos campos (e-mail, telefones, nome fantasia, situação especial) vêm `null` legitimamente.
- **Timeout de 10 s** no cliente e `Accept: application/json`.
- **Não dependa de ordem de chaves** no JSON nem de campos fora desta documentação; novos campos podem ser acrescentados sem aviso (nunca removidos sem mudança de versão).

## Conta, chaves e cobrança (rotas do painel)

Estas rotas existem para o [painel](https://apipj.com.br/painel) e usam sessão por cookie, não chave de API. Estão aqui por transparência; para integrar, use as rotas públicas acima.

| Rota | Descrição |
|---|---|
| `POST /v1/auth/signup` | `{email, password, name, cpf_cnpj?, accept_tos: true, tos_version?}` cria a conta, a sessão e a primeira chave. `tos_version` é a versão dos termos exibida; se não for a vigente, 400 `tos_required` |
| `POST /v1/auth/login` / `POST /v1/auth/logout` | Sessão web. O login devolve `tos_required: true` quando os termos mudaram desde o último aceite |
| `POST /v1/auth/accept-tos` | `{accept_tos: true}` registra o aceite da versão vigente dos termos (pedido quando `/v1/me` ou o login devolvem `tos_required: true`) |
| `POST /v1/auth/password` | `{current_password, new_password}` troca a senha e encerra as outras sessões |
| `GET /v1/auth/providers` | Provedores de login disponíveis (`{google: true}` ou `{google: false}`) |
| `GET /v1/auth/google/start`, `GET /v1/auth/google/callback` | Entrar, criar conta ou vincular a conta Google (OpenID Connect com PKCE); `POST /v1/auth/google/unlink` desvincula |
| `GET /v1/auth/google/pending`, `POST /v1/auth/google/complete` | Cadastro pelo Google iniciado na tela de login: `pending` mostra o e-mail a confirmar e `complete` com `{accept_tos: true, tos_version}` cria a conta |
| `GET /v1/me` | Usuário, plano, validade, uso do mês, `tos_required` e formas de entrar (senha, Google) |
| `GET /v1/keys`, `POST /v1/keys`, `DELETE /v1/keys/{id}` | Lista, cria (máximo 5 ativas) e revoga chaves |
| `GET /v1/usage` | Uso do período, limites e reset (aceita sessão ou chave) |
| `GET /v1/plans` | Planos públicos, limites e versão vigente dos termos (aberta, sem sessão) |
| `POST /v1/billing/checkout` | `{plan, cpf_cnpj?}` devolve `{url}` da página de pagamento do Asaas (cartão de crédito); `cpf_cnpj` é exigido quando a conta ainda não tem. Também serve para trocar de plano, sem pró-rata |
| `POST /v1/billing/cancel` | Cancela a assinatura; o plano continua até o fim do período pago. Com `{refund: true}`, desistência em até 7 dias da confirmação do primeiro pagamento do plano: estorno integral se o uso ficou em até 20% da cota mensal, proporcional à parte não usada acima disso, e volta imediata ao gratuito; fora das condições, 400 `refund_not_eligible` com `reason` |
| `POST /v1/account/close` | `{password}`, ou `{confirm_email}` em conta criada com o Google (sem senha), encerra a conta: cancela a assinatura, revoga as chaves e encerra as sessões. Carência de 30 dias, em que um novo login reativa a conta |

Rotas mutáveis exigem o header `X-Requested-With: apipj` e `Origin` da mesma origem. Nenhum dado de cartão passa pelos nossos servidores.

## Changelog

- **10/10/2026**: versão inicial da documentação (v1).
