
O emissaoja emite Nota Fiscal de Serviço eletrônica (NFS-e) no **Emissor Nacional** com o CNPJ e a senha do portal, **sem certificado digital**, para MEI e empresas do Simples Nacional. Esta API expõe o mesmo motor que o app usa: cadastre a empresa uma vez, crie tomadores e serviços, emita e cancele notas, baixe PDF e XML.

Tudo em JSON, em português, em `snake_case`. A referência completa, com todos os campos e códigos, está em [`/v1/openapi.json`](/v1/openapi.json) (OpenAPI 3.1). Esta página também existe em texto puro: [`/docs.md`](/docs.md) e [`/llms.txt`](/llms.txt), para agentes de IA.

## Em 5 minutos

1. Entre no app com a sua conta Google e aceite os termos: [emissaoja.com.br/app](/app).
2. Em **Conta → API**, crie uma chave. Ela aparece uma vez: guarde como senha.
3. Conecte a empresa com o CNPJ e a senha do Emissor Nacional (passo abaixo). O emissaoja confere a senha no portal, lê o cadastro fiscal e traz as notas dos últimos 90 dias.
4. Crie um tomador pelo CNPJ e emita a primeira nota.

```bash
# 1. Conectar a empresa (responde na hora; a verificação segue em segundo plano)
curl -X PUT https://emissaoja.com.br/v1/empresa/credencial \
  -H "Authorization: Bearer ej_live_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"cnpj":"11222333000181","senha":"senha-do-emissor-nacional"}'

# 2. Acompanhar até proximo_passo sair de "aguardar" (a cada 2 s, depois 5 s)
curl https://emissaoja.com.br/v1/empresa/credencial \
  -H "Authorization: Bearer ej_live_SUA_CHAVE"

# 3. Tomador pelo CNPJ (nome e endereço vêm da Receita)
curl -X POST https://emissaoja.com.br/v1/tomadores \
  -H "Authorization: Bearer ej_live_SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{"documento":"12345678000195"}'

# 4. Emitir (prévia + emissão num passo). Idempotency-Key evita nota em dobro.
curl -X POST https://emissaoja.com.br/v1/notas \
  -H "Authorization: Bearer ej_live_SUA_CHAVE" \
  -H "Idempotency-Key: pedido-2026-10-0001" \
  -H "Content-Type: application/json" \
  -d '{"tomador_tipo":"pj","tomador_id":"<id do tomador>","valor":1500,"descricao":"Consultoria de outubro"}'

# 5. Acompanhar o pedido até autorizada | recusada | incerta
curl https://emissaoja.com.br/v1/emissoes/<id do pedido> \
  -H "Authorization: Bearer ej_live_SUA_CHAVE"
```

A resposta do passo 4 é `202` com o `pedido`; a do passo 5 traz `estado`, `numero`, `chave_acesso` e `nota: { origem, id }`, o par que baixa o PDF em `GET /v1/notas/{origem}/{id}/pdf`.

## Autenticação

Toda chamada leva a chave no cabeçalho:

```
Authorization: Bearer ej_live_…
```

- A chave vale para **a empresa e o plano da conta** que a criou. Não há escopos: quem tem a chave emite em nome da empresa. Guarde-a no servidor, nunca no navegador nem em app de celular.
- Até 5 chaves ativas por conta. Revogar uma chave (no app, Conta → API) para na hora.
- Sem chave, chave inválida ou revogada: `401` com `code: "chave_invalida"`.
- As rotas `GET /v1/status` e `GET /v1/openapi.json` não exigem chave.

## Respostas e erros

Sucesso é `2xx` com JSON. Erro é sempre este envelope, e **o que importa é o `code`**, nunca o texto:

```json
{ "status": "ERROR", "code": "nota_bloqueada", "message": "Antes de emitir, resolva:", "bloqueios": [ { "code": "tomador_incompleto", "mensagem": "…", "acao": "completar_tomador" } ] }
```

| HTTP | Quando | Códigos comuns |
| --- | --- | --- |
| 400 | Campo inválido | `validation_error`, `valor_invalido`, `tomador_obrigatorio`, `cnpj_invalido`, `chave_idempotencia_invalida` |
| 401 | Chave | `chave_invalida` |
| 402 | Limite do plano no mês | `limite_notas` |
| 404 | Não existe | `tomador_nao_encontrado`, `servico_nao_encontrado`, `nota_nao_encontrada`, `pedido_nao_encontrado` |
| 409 | Estado atual não permite | `sem_empresa`, `aceite_pendente`, `emissao_repetida_recente`, `possivel_duplicada`, `incerta_pendente`, `ultimo_servico_ativo` |
| 422 | A nota não pode sair assim | `nota_bloqueada` (com `bloqueios[]`) |
| 429 | Limite por minuto | `rate_limited` (cabeçalho `Retry-After`) |
| 503 | Portal ou motor fora | `motor_indisponivel`, `fila_indisponivel` (repita com a mesma chave), `receita_indisponivel` |

A lista completa, com mensagens e ações, está no OpenAPI.

## Limites

- 60 chamadas por minuto por chave; emissões 10 por minuto; buscas e catálogo 120.
- Verificação de senha: 3 falhas por CNPJ em 24 h (regra do portal, para não bloquear a conta da empresa), 5 por conta, 10 por IP.
- Nota conta no plano quando o Emissor Nacional **aceita** o pedido; recusada não conta. O plano Grátis tem 1 nota por mês. Veja os planos em [emissaoja.com.br/#precos](/#precos).

## Empresa

Uma conta tem uma empresa. A API configura e acompanha o vínculo:

| Rota | O que faz |
| --- | --- |
| `GET /v1/empresa` | Empresa, estado da credencial (`ausente`, `verificando`, `ativa`, `congelada`), estado do onboarding, importação e avisos |
| `PUT /v1/empresa/credencial` | `{ cnpj, senha }` → `202` com a `tentativa`. Dispara a verificação no portal e o retro-seed (cadastro fiscal + notas dos últimos 90 dias). A senha nunca é guardada nem devolvida |
| `GET /v1/empresa/credencial` | A tentativa em andamento (ou a última): `estado` e `proximo_passo` (`aguardar`, `configurando`, `pronto`, `corrigir_senha`, `tentar_mais_tarde`, `redefinir_no_portal`, `suporte`) |
| `GET /v1/empresa/onboarding` | Estágios, perguntas pendentes e bloqueio |
| `PUT /v1/empresa/fiscal` | `{ aliquota_simples?, reg_ap_trib_sn? }`: as perguntas que o cadastro fiscal não responde sozinho |
| `POST /v1/empresa/reler-fisco` | Lê o cadastro fiscal de novo no portal |

`onboarding.estado` segue `configurando → perguntas → pronto`. Com `perguntas`, leia `GET /v1/empresa/onboarding` e responda em `PUT /v1/empresa/fiscal`. Emitir antes de `pronto` devolve `422 nota_bloqueada` com o que falta.

## Tomadores

| Rota | O que faz |
| --- | --- |
| `GET /v1/tomadores?busca=&pagina=&limite=&ordem=` | Lista paginada (`nome` ou `recentes`) |
| `GET /v1/tomadores/busca?q=` | Busca rápida por nome ou documento |
| `POST /v1/tomadores` | `{ documento }` para CNPJ (nome e endereço vêm da Receita) ou `{ documento, nome }` para CPF. Já existia → `200` com `ja_existia: true` |
| `GET /v1/tomadores/{tipo}/{id}` | Ficha (`tipo` = `pj` ou `pf`) |
| `PUT /v1/tomadores/{tipo}/{id}` | Edição parcial de endereço e contato. O documento é imutável |

Tomador não se apaga: notas emitidas apontam para ele.

## Serviços

| Rota | O que faz |
| --- | --- |
| `GET /v1/servicos?ativos=1|0|todos` | Lista |
| `POST /v1/servicos` | `{ descricao, c_trib_nac, trib_issqn, retem_iss, nbs?, c_trib_mun?, tp_oper?, aliquota_iss? }` |
| `PUT /v1/servicos/{id}` | Edição parcial |
| `DELETE /v1/servicos/{id}` | Desativa (o último ativo não pode: `ultimo_servico_ativo`) |
| `GET /v1/catalogo/servicos?q=` | Catálogo nacional de códigos de serviço, por texto |

`trib_issqn` aceita `tributavel` e `nao_incidencia`. Isenção, imunidade e exportação ficam fora do emissaoja.

## Notas

| Rota | O que faz |
| --- | --- |
| `POST /v1/notas` | **Emitir**: prévia + emissão. Cabeçalho `Idempotency-Key` obrigatório. `202 { pedido, previa }` |
| `GET /v1/emissoes/{id}` | Acompanhar o pedido: `na_fila → emitindo → confirmando → autorizada | recusada | incerta` |
| `GET /v1/notas?periodo=&situacao=&busca=&cursor=` | Lista (locais e importadas), paginada por cursor |
| `GET /v1/notas/{origem}/{id}/pdf` | DANFSe em PDF |
| `GET /v1/notas/{origem}/{id}/xml` | XML da nota |
| `POST /v1/notas/{origem}/{id}/cancelar` | `{ motivo: "1" | "2", justificativa? }` → `202` com a `operacao`; acompanhe em `GET /v1/operacoes/{id}` |

Corpo de `POST /v1/notas`:

| Campo | Obrigatório | Observação |
| --- | --- | --- |
| `tomador_tipo`, `tomador_id` | sim | `pj` ou `pf` + id do tomador |
| `valor` | sim | Reais, até 2 casas |
| `servico_id` | não | Omitido: o serviço principal |
| `descricao` | não | Até 2000 caracteres; omitida: a descrição do serviço |
| `competencia` | não | `AAAA-MM-DD`; omitida: hoje |
| `retem_iss`, `aliquota_iss`, `municipio_prestacao` | não | Só quando um bloqueio pedir |

Nota não se altera: para corrigir, cancele e emita outra. Nota autorizada e depois cancelada conta no plano; recusada não.

**Idempotência.** Use uma `Idempotency-Key` por nota que você quer emitir (um id do seu pedido serve). Repetir a mesma chave devolve `200` com o mesmo `pedido` e `repetido: true`; nunca emite duas vezes. Se receber `503 fila_indisponivel`, repita **com a mesma chave**.

**Nota em dobro.** Nota igual (mesmo tomador, valor e competência) no mesmo dia volta `409 possivel_duplicada` com `nota_igual_recente`; se for mesmo outra, repita com `permitir_repetida_de` = o `pedido_id` informado. Mesma assinatura em menos de 2 minutos volta `409 emissao_repetida_recente`; repita com `confirmo_repeticao: true`.

## Ambiente de testes

`https://sandbox.emissaoja.com.br` é o mesmo emissaoja com um **Emissor Nacional simulado**: nada ali existe no fisco e nada é cobrado. Crie a conta com o Google, conecte uma das empresas de exemplo e use a API, o MCP e o app como em produção. As chaves do sandbox começam com `ej_test_` e só valem lá; as de produção, `ej_live_`, só valem em produção. `GET /v1/status` diz `"ambiente": "sandbox"`.

| CNPJ de exemplo | Senha | O que acontece |
| --- | --- | --- |
| `11111111000191` | qualquer | MEI pronto para emitir, com notas e clientes dos últimos 90 dias |
| `22222222000191` | qualquer | ME/EPP que pede a alíquota do Simples antes de emitir |
| `33333333000191` | qualquer | MEI sem notas anteriores |
| `44444444000191` | qualquer | Empresa bloqueada (não optante do Simples) |
| qualquer um | `errada` | Senha recusada pelo portal |

Valores com significado ao emitir: `13,13` nota incerta (confira com `POST /v1/emissoes/{id}/conferir` → autorizada), `77,77` incerta que não saiu, `99,99` recusada pelo fisco. Tomadores de exemplo: CNPJ `12345678000195`, CPF `52998224725`. O sandbox é reiniciado a cada publicação do emissaoja: a empresa continua conectada, mas as notas emitidas antes somem.

## Consulta de CNPJ

`GET /v1/cnpj/{cnpj}` devolve os dados cadastrais da Receita Federal (razão social, situação, CNAEs, endereço, Simples, MEI, sócios com CPF mascarado), pela base do [apipj](https://apipj.com.br). Não cria tomador; serve para validar antes de cadastrar. 30 consultas por minuto por chave.

## MCP: emitir pelo seu assistente

O emissaoja também é um servidor MCP (Model Context Protocol) em `https://emissaoja.com.br/mcp`: Claude Code, Cursor, VS Code e outros clientes que mandam um cabeçalho fixo conectam com a mesma chave de API. As ferramentas são as rotas desta API: ver a empresa, conectar a credencial, listar e criar tomadores, listar serviços, emitir, acompanhar, listar e cancelar notas, consultar CNPJ. Emitir e cancelar têm efeito fiscal real: o assistente confirma com você antes.

```bash
claude mcp add --transport http emissaoja https://emissaoja.com.br/mcp \
  --header "Authorization: Bearer ej_live_SUA_CHAVE"
```

Cursor e VS Code (`mcp.json`):

```json
{ "mcpServers": { "emissaoja": { "url": "https://emissaoja.com.br/mcp", "headers": { "Authorization": "Bearer ej_live_SUA_CHAVE" } } } }
```

No **claude.ai** (Configurações → Conectores → Adicionar conector personalizado) e no **ChatGPT** (modo desenvolvedor), informe só a URL `https://emissaoja.com.br/mcp`: o assistente pede para conectar, você entra com o Google e autoriza. A conexão vale 60 dias sem uso e pode ser desfeita em Conta → API.

## Exemplos

JavaScript (Node 18+ ou qualquer runtime com `fetch`):

```javascript
const BASE = "https://emissaoja.com.br";
const cabecalhos = { Authorization: `Bearer ${process.env.EMISSAOJA_KEY}`, "Content-Type": "application/json" };

async function emitir({ tomadorTipo, tomadorId, valor, descricao, chave }) {
  const r = await fetch(`${BASE}/v1/notas`, {
    method: "POST",
    headers: { ...cabecalhos, "Idempotency-Key": chave },
    body: JSON.stringify({ tomador_tipo: tomadorTipo, tomador_id: tomadorId, valor, descricao }),
  });
  const corpo = await r.json();
  if (!r.ok) throw new Error(`${corpo.code}: ${corpo.message}`);
  return corpo.pedido;
}

async function aguardar(pedidoId) {
  for (;;) {
    const r = await fetch(`${BASE}/v1/emissoes/${pedidoId}`, { headers: cabecalhos });
    const { pedido } = await r.json();
    if (["autorizada", "recusada", "incerta"].includes(pedido.estado)) return pedido;
    await new Promise((ok) => setTimeout(ok, pedido.proxima_consulta_ms ?? 3000));
  }
}
```

Python (`requests`):

```python
import os, time, requests

BASE = "https://emissaoja.com.br"
H = {"Authorization": f"Bearer {os.environ['EMISSAOJA_KEY']}"}

def emitir(tomador_tipo, tomador_id, valor, descricao, chave):
    r = requests.post(f"{BASE}/v1/notas", headers={**H, "Idempotency-Key": chave},
                      json={"tomador_tipo": tomador_tipo, "tomador_id": tomador_id, "valor": valor, "descricao": descricao}, timeout=30)
    corpo = r.json()
    if not r.ok:
        raise RuntimeError(f"{corpo['code']}: {corpo['message']}")
    return corpo["pedido"]

def aguardar(pedido_id):
    while True:
        pedido = requests.get(f"{BASE}/v1/emissoes/{pedido_id}", headers=H, timeout=30).json()["pedido"]
        if pedido["estado"] in ("autorizada", "recusada", "incerta"):
            return pedido
        time.sleep((pedido.get("proxima_consulta_ms") or 3000) / 1000)
```

## Perguntas

**Precisa de certificado digital?** Não. O emissaoja entra no Emissor Nacional com o CNPJ e a senha do próprio portal.

**A senha fica guardada?** Durante a verificação ela fica só na memória do servidor; depois, cifrada no motor fiscal, para emitir quando você pedir. Ela nunca aparece em resposta nem em log. Encerrar a conta apaga a senha.

**Tem ambiente de testes?** Sim: `https://sandbox.emissaoja.com.br`, com o motor simulado (veja a seção Ambiente de testes).

**E webhooks?** Ainda não; acompanhe o pedido em `GET /v1/emissoes/{id}` usando `proxima_consulta_ms`. Webhooks assinados estão no plano.

**Posso ter várias empresas numa conta?** Não. Uma conta Google tem uma empresa. Para várias empresas, uma conta e uma chave por empresa.

**Dúvidas:** [suporte@emissaoja.com.br](mailto:suporte@emissaoja.com.br). O emissaoja é um serviço privado e não tem vínculo com a Receita Federal nem com o Emissor Nacional.
