openapi: 3.1.0 info: title: API Confluxo (Ecossistema) version: '2026-08-26' summary: API do sistema contábil Confluxo, para integradores externos. description: | Esta especificação descreve **apenas** o que foi confirmado por leitura do código-fonte do backend em 26/08/2026. Onde o comportamento não pôde ser provado, a descrição começa com `NÃO CONFIRMADO:`. ## O produto O produto se chama **Confluxo**. A **Contabilin** deixou de ser o produto e passou a ser um dos escritórios clientes dentro dele. Textos, tabelas e nomes de variável ainda dizem "Contabilin" em muitos lugares — isso é legado, não significa outro sistema. ## Não existe versionamento de API Não há `Accept-version`, nem `/v2`, nem changelog de contrato. O prefixo `/api/v1/ferramentas` é o nome da rota, não uma versão negociável. Mudanças de contrato chegam sem aviso. ## Estilos que coexistem (nada aqui é REST completo) | Superfície | Estilo | Verbos que existem | |---|---|---| | `/api/v1/ferramentas` | RPC puro (nome da operação no path, input no corpo) | GET (catálogo) + POST | | `/api/cliente/v1` | híbrido: GET por recurso, POST por ação | GET + POST | | `/api/portal-api` | o mais REST-like, ainda assim só | GET + POST | | `/api/clients` | caminhos que parecem REST mas são verbos (`/cancel`, `/status`) | GET + POST + 1 DELETE | Não existe `PUT` nem `PATCH` em nenhuma superfície de negócio (o único `PATCH` é o de revogação de chave). Não existe `201`, nem `Location`, nem `ETag`, nem `If-Modified-Since`. ## Superfície recomendada para um ERP/sistema externo **`GET /api/v1/ferramentas` + `POST /api/v1/ferramentas/{nome}`**, com uma chave de `mcp_tokens` emitida por um administrador do escritório. É a única superfície que: alcança a carteira inteira do escritório, tem escopo por módulo, tem teto de 120 chamadas/min por chave e tem auditoria por operador. O que ela **não** faz: **não cria empresa**. Não existe ferramenta nem rota REST de criação genérica de cliente (ver seção `Criação de empresa`). contact: name: Escritório responsável pela integração url: https://ecossistema.contabilin.com.br servers: - url: https://api.confluxo.app.br description: | Host novo (Confluxo). **Preferir este.** Medido em 26/08/2026: serve exatamente o mesmo backend que `api.contabilin.com.br` (mesmo commit, mesmo processo). Os dois são intercambiáveis. - url: https://api.contabilin.com.br description: | Host legado (nome antigo do produto). Mesmo backend, mesmo commit, mesmo processo do host do Confluxo — intercambiável com ele. Continua no ar; use só se já estiver integrado. tags: - name: Ferramentas (chave de operador) description: | RPC do escritório. Alcança a carteira inteira. Chave da tabela `mcp_tokens`, prefixo `cnt_`. **É a superfície recomendada para integração de sistema externo.** - name: MCP JSON-RPC description: | Mesma autenticação, mesmas travas e mesma auditoria das rotas REST equivalentes — só muda a fachada, que é JSON-RPC 2.0 para consumo por LLM. **Não use isto para ERP.** - name: Chaves e auditoria description: Emissão, listagem, revogação e trilha de uso das chaves. - name: Empresas (rotas cruas) description: | Rotas `/api/clients`. Autenticação por JWT de operador (sessão humana, expira) ou pela chave interna do processo. **Não são credencial de máquina para terceiro.** - name: Criação de empresa description: Os únicos dois caminhos HTTP que criam empresa. Ambos são de nicho. - name: Faturamento e apuração description: Faturamento por empresa, por período e por competência. - name: Contrato e honorário description: Valor mensal contratado e conferência contra o Asaas. - name: API da empresa (chave por empresa) description: | `/api/cliente/v1`. Mono-empresa por construção: a empresa sai do token e **nenhum recurso aceita CNPJ como parâmetro**. Chave da tabela `cliente_api_tokens`, prefixo `ctb_`. - name: portal-api (server↔server) description: | Camada interna consumida pelo painel do cliente. Chave global de processo, sem escopo e sem auditoria por consumidor. **Não entregar a terceiro** — quem tem a chave alcança qualquer CNPJ. - name: Diagnóstico description: Rotas públicas de saúde e versão. security: - ChaveOperadorBearer: [] paths: ############################################################################ # FERRAMENTAS — superfície recomendada ############################################################################ /api/v1/ferramentas: get: tags: [Ferramentas (chave de operador)] operationId: listarFerramentas summary: Catálogo auto-descrito das ferramentas que a sua chave alcança description: | Devolve só as ferramentas que passam nas duas travas da chave: 1. o módulo da ferramenta está entre os módulos da chave; 2. se a ferramenta escreve, a chave é de escopo `completo`. Ferramenta fora do alcance **some da lista** (não aparece marcada como bloqueada). Cada item traz `parametros`, que já é um JSON Schema — é a fonte da verdade para o corpo de `POST /api/v1/ferramentas/{nome}`. **Sempre leia este catálogo antes de montar chamadas**; ele reflete o estado real do servidor, esta especificação não. Escopo exigido: qualquer (`leitura` basta). x-escopo: leitura security: - ChaveOperadorBearer: [] - ChaveOperadorHeader: [] responses: '200': description: Catálogo filtrado pela chave. content: application/json: schema: type: object description: | NÃO CONFIRMADO: no código só foi possível ver o campo `ferramentas` sendo montado; a resposta pode trazer outros campos ao lado dele. Por isso `additionalProperties` fica aberto. properties: ferramentas: type: array items: $ref: '#/components/schemas/FerramentaDoCatalogo' additionalProperties: true example: ferramentas: - nome: empresas_listar modulo: empresas escreve: false descricao: Lista as empresas da carteira. parametros: type: object properties: regime: { type: string } situacao: { type: string } limite: { type: number } '401': $ref: '#/components/responses/NaoAutenticado' '429': $ref: '#/components/responses/MuitasChamadas' /api/v1/ferramentas/{nome}: post: tags: [Ferramentas (chave de operador)] operationId: executarFerramenta summary: Executa uma ferramenta pelo nome description: | RPC: o nome da operação vai no path, **todo** o input vai no corpo JSON. Limite de corpo: 25 MB. O corpo aceito por cada ferramenta é o `parametros` (JSON Schema) devolvido por `GET /api/v1/ferramentas`. Esta especificação declara o corpo explicitamente só para as ferramentas cujo schema foi confirmado no código — para as demais, o corpo aqui é um objeto livre. Escopo exigido: `leitura` para as 27 ferramentas de consulta; `completo` para as 24 que escrevem. Módulo exigido: o módulo da ferramenta (ver a tabela em `API.md`). Ordem das checagens: 401 (chave) → 429 (teto) → 404 (ferramenta) → 403 (módulo) → 403 (escrita) → 422 (erro de execução). Ou seja, **não dá para enumerar nomes de ferramenta sem uma chave válida**. security: - ChaveOperadorBearer: [] - ChaveOperadorHeader: [] parameters: - name: nome in: path required: true description: | Nome da ferramenta. As 51 existentes estão no enum. Confira sempre contra `GET /api/v1/ferramentas` — o catálogo é a fonte da verdade. schema: type: string enum: # módulo contabil (21) - clientes_buscar - mes_situacao - extrato_pendentes - plano_contas - regras_aprendidas - a_conferir - consultar_tabela - classificar - regra_corrigir - regra_excluir - conta_criar - conta_desativar - contas_mesclar - lancamento_manual - derivar - conciliar - banco_sincronizar - extrato_enviar - auto_import_confirmar - mes_fechar - mes_reabrir # módulo empresas (6) - empresa_ficha - empresa_socios - empresa_certificado - empresas_vencendo - empresas_listar - empresa_atualizar_receita # módulo fiscal (10) - fiscal_apuracao - fiscal_guias_em_aberto - fiscal_notas - fiscal_certidoes - mei_das_situacao - fiscal_gerar_das_mei - fiscal_guias_prontas - fiscal_enviar_guias - fiscal_fechar_apuracao - fiscal_reabrir_apuracao # módulo folha (6) - folha_funcionarios - folha_mes - folha_movimentacoes - folha_calcular - folha_esocial_transmitir - folha_esocial_consultar_lote # módulo financeiro (4) - financeiro_honorarios - financeiro_sem_cobranca - financeiro_contratos - financeiro_criar_cobranca # módulo incubadora (4) - incubadora_processos - incubadora_avancar - incubadora_anotar - incubadora_processo example: empresas_listar requestBody: required: false description: | Argumentos da ferramenta. Objeto livre aqui porque cada ferramenta tem o seu schema — use o `parametros` do catálogo. content: application/json: schema: type: object additionalProperties: true example: regime: Simples Nacional limite: 50 responses: '200': $ref: '#/components/responses/FerramentaOk' '401': $ref: '#/components/responses/NaoAutenticado' '403': $ref: '#/components/responses/SemPermissao' '404': description: Ferramenta desconhecida. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: ok: false error: 'ferramenta desconhecida: empresa_criar' '422': $ref: '#/components/responses/ErroDeExecucao' '429': $ref: '#/components/responses/MuitasChamadas' /api/v1/ferramentas/empresas_listar: post: tags: [Ferramentas (chave de operador)] operationId: ferramentaEmpresasListar summary: Lista as empresas da carteira do escritório description: | Módulo `empresas` · escopo `leitura` · não escreve. Filtra por escritório (usa a org do dono da chave). Sem `situacao`, exclui as arquivadas. x-modulo: empresas x-escopo: leitura security: - ChaveOperadorBearer: [] - ChaveOperadorHeader: [] requestBody: required: false content: application/json: schema: type: object properties: regime: type: string description: 'Filtro de regime. NÃO CONFIRMADO: se aceita valor parcial ou só exato.' example: Simples Nacional situacao: type: string description: | Filtro de status. Quando ausente, aplica `status != archived`. example: active limite: type: integer description: Máximo de linhas. Padrão 100. default: 100 additionalProperties: false responses: '200': description: Lista de empresas. content: application/json: schema: type: object properties: ok: { const: true } resultado: type: array items: { $ref: '#/components/schemas/EmpresaListada' } required: [ok, resultado] example: ok: true resultado: - name: AGENCIA MAXIMUS LTDA cnpj: '12345678000190' regime: Simples Nacional anexo: III status: active municipio: Blumenau uf: SC monthly_value: 347 '401': { $ref: '#/components/responses/NaoAutenticado' } '403': { $ref: '#/components/responses/SemPermissao' } '422': { $ref: '#/components/responses/ErroDeExecucao' } '429': { $ref: '#/components/responses/MuitasChamadas' } /api/v1/ferramentas/clientes_buscar: post: tags: [Ferramentas (chave de operador)] operationId: ferramentaClientesBuscar summary: Acha empresas por nome ou CNPJ description: | Módulo `contabil` (a ferramenta não declara módulo, e o padrão é contábil) · escopo `leitura` · não escreve. Se `busca` tiver 6 ou mais dígitos, procura por CNPJ (`LIKE %digitos%`); senão procura por nome (`ILIKE %busca%`). Sempre exclui `status = archived`. ⚠️ **Esta ferramenta não aplica o filtro de escritório explicitamente** (ao contrário de `empresas_listar`). O isolamento depende do cliente de banco estar em modo escopado — ver `NÃO CONFIRMADO` em `API.md`. Se o seu integrador só pode ver um escritório, prefira `empresas_listar`. x-modulo: contabil x-escopo: leitura security: - ChaveOperadorBearer: [] - ChaveOperadorHeader: [] requestBody: required: false content: application/json: schema: type: object properties: busca: type: string description: Nome (parcial) ou CNPJ (parcial, só dígitos). example: '12345678' limite: type: integer default: 20 additionalProperties: false responses: '200': description: Empresas encontradas. content: application/json: schema: type: object properties: ok: { const: true } resultado: type: array items: { $ref: '#/components/schemas/EmpresaBusca' } required: [ok, resultado] example: ok: true resultado: - id: 6f1f0f5a-1c3e-4a2b-9f0d-6b6a2f0f9c11 name: AGENCIA MAXIMUS LTDA cnpj: '12345678000190' regime: Simples Nacional status: active '401': { $ref: '#/components/responses/NaoAutenticado' } '403': { $ref: '#/components/responses/SemPermissao' } '422': { $ref: '#/components/responses/ErroDeExecucao' } '429': { $ref: '#/components/responses/MuitasChamadas' } /api/v1/ferramentas/empresa_ficha: post: tags: [Ferramentas (chave de operador)] operationId: ferramentaEmpresaFicha summary: Ficha completa de uma empresa description: | Módulo `empresas` · escopo `leitura` · não escreve. `cliente` é obrigatório e aceita nome ou CNPJ (o servidor resolve). Se não achar, a ferramenta lança e a resposta é **422**, não 404. Devolve cerca de 30 campos. Os confirmados por leitura de código estão no schema; o objeto pode trazer mais, por isso `additionalProperties` fica aberto. x-modulo: empresas x-escopo: leitura security: - ChaveOperadorBearer: [] - ChaveOperadorHeader: [] requestBody: required: true content: application/json: schema: type: object properties: cliente: type: string description: Nome ou CNPJ da empresa. example: '12345678000190' required: [cliente] additionalProperties: false responses: '200': description: Ficha da empresa. content: application/json: schema: type: object properties: ok: { const: true } resultado: { $ref: '#/components/schemas/EmpresaFicha' } required: [ok, resultado] '401': { $ref: '#/components/responses/NaoAutenticado' } '403': { $ref: '#/components/responses/SemPermissao' } '422': description: | Erro de execução — inclui "empresa não encontrada" e falta do argumento `cliente`. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: ok: false error: 'não achei o cliente "Maximus"' '429': { $ref: '#/components/responses/MuitasChamadas' } /api/v1/ferramentas/empresa_socios: post: tags: [Ferramentas (chave de operador)] operationId: ferramentaEmpresaSocios summary: Quadro societário de uma empresa description: Módulo `empresas` · escopo `leitura` · não escreve. x-modulo: empresas x-escopo: leitura security: - ChaveOperadorBearer: [] - ChaveOperadorHeader: [] requestBody: required: true content: application/json: schema: type: object properties: cliente: { type: string, example: '12345678000190' } required: [cliente] additionalProperties: false responses: '200': description: Quadro societário. content: application/json: schema: type: object properties: ok: { const: true } resultado: type: object properties: empresa: { type: string } cnpj: { type: string } capital_social: type: [number, 'null'] natureza_juridica: type: [string, 'null'] socios: type: array description: | Conteúdo do campo `qsa` do cadastro. NÃO CONFIRMADO: shape de cada sócio (vem da Receita e é gravado como veio). items: type: object additionalProperties: true additionalProperties: true required: [ok, resultado] '401': { $ref: '#/components/responses/NaoAutenticado' } '403': { $ref: '#/components/responses/SemPermissao' } '422': { $ref: '#/components/responses/ErroDeExecucao' } '429': { $ref: '#/components/responses/MuitasChamadas' } /api/v1/ferramentas/empresas_vencendo: post: tags: [Ferramentas (chave de operador)] operationId: ferramentaEmpresasVencendo summary: Empresas com certificado ou procuração vencendo description: Módulo `empresas` · escopo `leitura` · não escreve. x-modulo: empresas x-escopo: leitura security: - ChaveOperadorBearer: [] - ChaveOperadorHeader: [] requestBody: required: false content: application/json: schema: type: object properties: dias: type: integer default: 30 o_que: type: string enum: [certificado, procuracao, ambos] additionalProperties: false responses: '200': description: | Lista de empresas. NÃO CONFIRMADO: shape exato de cada item. content: application/json: schema: { $ref: '#/components/schemas/RespostaFerramenta' } '401': { $ref: '#/components/responses/NaoAutenticado' } '403': { $ref: '#/components/responses/SemPermissao' } '422': { $ref: '#/components/responses/ErroDeExecucao' } '429': { $ref: '#/components/responses/MuitasChamadas' } /api/v1/ferramentas/empresa_atualizar_receita: post: tags: [Ferramentas (chave de operador)] operationId: ferramentaEmpresaAtualizarReceita summary: Sincroniza o cadastro de uma empresa com a Receita description: | Módulo `empresas` · escopo **`completo`** · **escreve**. É a **única** ferramenta de escrita do módulo `empresas`, e ela é estreita: não aceita campos arbitrários. Internamente chama `POST /api/clients/{id}/atualizar-receita`, que busca o CNPJ na Receita (BrasilAPI, sem custo) e grava o que voltou. Regras herdadas dessa rota: **nunca sobrescreve telefone/e-mail já preenchidos no painel** (a divergência é só reportada) e campo vazio na Receita não apaga o que existe. NÃO CONFIRMADO: o nome exato do parâmetro de entrada. As demais ferramentas do módulo usam `cliente`. **Leia o `parametros` desta ferramenta em `GET /api/v1/ferramentas` antes de chamar.** x-modulo: empresas x-escopo: completo security: - ChaveOperadorBearer: [] - ChaveOperadorHeader: [] requestBody: required: true content: application/json: schema: type: object additionalProperties: true description: 'NÃO CONFIRMADO: schema exato. Ver o catálogo.' example: cliente: '12345678000190' responses: '200': { $ref: '#/components/responses/FerramentaOk' } '401': { $ref: '#/components/responses/NaoAutenticado' } '403': description: | Chave sem o módulo `empresas`, ou chave de `leitura` tentando escrever. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: ok: false error: esta chave é somente leitura — posso consultar e montar o passo a passo, mas quem aplica é você na tela '422': { $ref: '#/components/responses/ErroDeExecucao' } '429': { $ref: '#/components/responses/MuitasChamadas' } /api/v1/ferramentas/financeiro_honorarios: post: tags: [Ferramentas (chave de operador), Contrato e honorário] operationId: ferramentaFinanceiroHonorarios summary: Honorários das empresas ativas description: | Módulo `financeiro` · escopo `leitura` · não escreve. Lê `clients.monthly_value` e campos vizinhos das empresas com `status = active`, ordenadas por nome, teto interno de 500 linhas. ⚠️ **Fonte secundária.** O valor contratado de verdade mora no contrato assinado (`public_contracts.extra_data.monthly_value`), não no cadastro. Uma medição de 21/08/2026 registrada no código diz que `clients.monthly_value` estava preenchido em 7 de 127 empresas ativas, enquanto o contrato tinha o valor nos 56 contratos. Para o valor contratado, prefira a rota de contrato. `total_empresas` e `receita_mensal` são sempre sobre a carteira inteira (até o teto de 500); `limite` corta só as linhas listadas. x-modulo: financeiro x-escopo: leitura security: - ChaveOperadorBearer: [] - ChaveOperadorHeader: [] requestBody: required: false content: application/json: schema: type: object properties: cliente: { type: string, description: Nome ou CNPJ, para filtrar uma empresa. } limite: { type: integer } additionalProperties: false responses: '200': description: Honorários. content: application/json: schema: type: object properties: ok: { const: true } resultado: type: object properties: total_empresas: { type: integer } receita_mensal: { type: number, description: Soma em reais. } em_permuta: { type: integer } listadas: { type: integer } empresas: type: array items: { $ref: '#/components/schemas/HonorarioEmpresa' } additionalProperties: true required: [ok, resultado] '401': { $ref: '#/components/responses/NaoAutenticado' } '403': { $ref: '#/components/responses/SemPermissao' } '422': { $ref: '#/components/responses/ErroDeExecucao' } '429': { $ref: '#/components/responses/MuitasChamadas' } /api/v1/ferramentas/financeiro_sem_cobranca: post: tags: [Ferramentas (chave de operador), Contrato e honorário] operationId: ferramentaFinanceiroSemCobranca summary: Empresas ativas com honorário e sem cobrança no Asaas description: | Módulo `financeiro` · escopo `leitura` · não escreve. Filtra `status = active`, `monthly_value > 0` e `asaas_id` ausente. Teto interno 500. NÃO CONFIRMADO: parâmetros aceitos (nenhum foi visto no código) e shape do item. x-modulo: financeiro x-escopo: leitura security: - ChaveOperadorBearer: [] - ChaveOperadorHeader: [] requestBody: required: false content: application/json: schema: type: object additionalProperties: true responses: '200': { $ref: '#/components/responses/FerramentaOk' } '401': { $ref: '#/components/responses/NaoAutenticado' } '403': { $ref: '#/components/responses/SemPermissao' } '422': { $ref: '#/components/responses/ErroDeExecucao' } '429': { $ref: '#/components/responses/MuitasChamadas' } /api/v1/ferramentas/financeiro_contratos: post: tags: [Ferramentas (chave de operador), Contrato e honorário] operationId: ferramentaFinanceiroContratos summary: Contratos registrados na tabela `contracts` description: | Módulo `financeiro` · escopo `leitura` · não escreve. Ordena por `created_at` desc; limite padrão 50. ⚠️ Esta ferramenta lê a tabela `contracts`, que **é uma terceira fonte** de valor mensal — diferente de `clients.monthly_value` (o cadastro) e de `public_contracts.extra_data.monthly_value` (o contrato assinado, a fonte de verdade). As três podem divergir. x-modulo: financeiro x-escopo: leitura security: - ChaveOperadorBearer: [] - ChaveOperadorHeader: [] requestBody: required: false content: application/json: schema: type: object properties: cliente: { type: string } status: { type: string } limite: { type: integer, default: 50 } additionalProperties: false responses: '200': description: Contratos. content: application/json: schema: type: object properties: ok: { const: true } resultado: type: array items: { $ref: '#/components/schemas/ContratoTabela' } required: [ok, resultado] '401': { $ref: '#/components/responses/NaoAutenticado' } '403': { $ref: '#/components/responses/SemPermissao' } '422': { $ref: '#/components/responses/ErroDeExecucao' } '429': { $ref: '#/components/responses/MuitasChamadas' } /api/v1/ferramentas/consultar_tabela: post: tags: [Ferramentas (chave de operador)] operationId: ferramentaConsultarTabela summary: Consulta livre em uma tabela liberada description: | Ferramenta **transversal**: passa a trava de módulo com qualquer módulo marcado na chave. O alcance real é decidido **por tabela**, e o mapa **nega por padrão** — tabela sem regra é inalcançável por qualquer chave de API. Três travas em série, todas com resposta **422** (não 403): 1. tabela de credenciais → bloqueada; 2. tabela fora da lista de legíveis → bloqueada, e a mensagem lista as liberadas; 3. tabela liberada mas de módulo que a sua chave não tem → bloqueada. Colunas sensíveis são removidas do resultado e a linha ganha `_omitido: "campos com credencial não são expostos"`. Mapa de módulo por tabela (confirmado): prefixo primeiro — `contabil_` → contabil; `legalizacao_` → incubadora; `nfse_`, `nfe_`, `mei_`, `obrigacoes_`, `regras_st`, `caixa_postal_` → fiscal; `esocial_`, `folha` → folha; `indicacao_` → financeiro. Exceções por nome: `clients` → empresas, `apuracoes` → fiscal, `employees` → folha, `contracts` → financeiro, `banco_conexoes` → contabil, `crm_oportunidades` → incubadora. NÃO CONFIRMADO: os demais parâmetros além de `tabela` (filtros, colunas, limite). Ver o catálogo. x-modulo: transversal x-escopo: leitura security: - ChaveOperadorBearer: [] - ChaveOperadorHeader: [] requestBody: required: true content: application/json: schema: type: object properties: tabela: type: string description: Tabela a consultar. Só as 21 liberadas. enum: - clients - contabil_extrato - contabil_lancamentos - contabil_plano_contas - contabil_extrato_regras - contabil_fechamento - contabil_categorias - contabil_partidas - contabil_implantacao - contabil_depara_cliente - contabil_encerramento - nfe_xmls - nfse_emitidas - apuracoes - folhas_mensais - mei_das - lancamentos_avulsos - extrato_solicitacoes - contabil_relatorios_enviados - banco_conexoes required: [tabela] additionalProperties: true responses: '200': { $ref: '#/components/responses/FerramentaOk' } '401': { $ref: '#/components/responses/NaoAutenticado' } '403': { $ref: '#/components/responses/SemPermissao' } '422': description: | Tabela bloqueada, não liberada, ou de módulo que a chave não alcança. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: ok: false error: 'tabela não liberada: operators. Liberadas: clients, contabil_extrato, ...' '429': { $ref: '#/components/responses/MuitasChamadas' } /api/v1/ferramentas/fiscal_apuracao: post: tags: [Ferramentas (chave de operador), Faturamento e apuração] operationId: ferramentaFiscalApuracao summary: Apuração fiscal de uma empresa description: | Módulo `fiscal` · escopo `leitura` · não escreve. Lê a tabela `apuracoes` (`competencia, regime, status, faturamento, das_calculado, das_status`, entre outros). ⚠️ **Cuidado com o campo `faturamento`.** Ele é uma coluna real da tabela, mas o motor de apuração grava o consolidado em `faturamento_total`, que cai dentro do JSONB `extra_data` — e esta ferramenta seleciona a coluna. O resultado pode vir **0** com o painel mostrando o valor certo. Para faturamento confiável, use `GET /api/apuracao/{cnpj}/faturamento-periodo` ou `GET /api/portal-api/{cnpj}/faturamento`. NÃO CONFIRMADO: parâmetros de entrada. Ver o catálogo. x-modulo: fiscal x-escopo: leitura security: - ChaveOperadorBearer: [] - ChaveOperadorHeader: [] requestBody: required: false content: application/json: schema: type: object additionalProperties: true responses: '200': { $ref: '#/components/responses/FerramentaOk' } '401': { $ref: '#/components/responses/NaoAutenticado' } '403': { $ref: '#/components/responses/SemPermissao' } '422': { $ref: '#/components/responses/ErroDeExecucao' } '429': { $ref: '#/components/responses/MuitasChamadas' } /api/v1/ferramentas/fiscal_guias_em_aberto: post: tags: [Ferramentas (chave de operador), Faturamento e apuração] operationId: ferramentaFiscalGuiasEmAberto summary: Guias em aberto da carteira description: | Módulo `fiscal` · escopo `leitura` · não escreve. NÃO CONFIRMADO: parâmetros de entrada e shape da saída. Ver o catálogo. x-modulo: fiscal x-escopo: leitura security: - ChaveOperadorBearer: [] - ChaveOperadorHeader: [] requestBody: required: false content: application/json: schema: type: object additionalProperties: true responses: '200': { $ref: '#/components/responses/FerramentaOk' } '401': { $ref: '#/components/responses/NaoAutenticado' } '403': { $ref: '#/components/responses/SemPermissao' } '422': { $ref: '#/components/responses/ErroDeExecucao' } '429': { $ref: '#/components/responses/MuitasChamadas' } ############################################################################ # MCP JSON-RPC ############################################################################ /api/mcp: post: tags: [MCP JSON-RPC] operationId: mcpOperador summary: Fachada JSON-RPC 2.0 das mesmas 51 ferramentas description: | Mesma chave, mesmo catálogo, mesmas travas e mesma auditoria de `POST /api/v1/ferramentas/{nome}` — só muda o envelope. **Feito para LLM; para um ERP use as rotas REST.** Diferenças de comportamento que importam: - `ping` e `initialize` respondem **antes** da autenticação (conectam sem chave); - em `tools/list`, ferramenta fora do alcance **some da lista**; - em `tools/call` sem permissão, a resposta é **HTTP 200** com `result.isError = true` (não 403); - `notifications/*` responde **202 com corpo vazio**; - método desconhecido responde HTTP 200 com `error.code = -32601`; - saída truncada em 120.000 caracteres. security: - ChaveOperadorBearer: [] - ChaveOperadorHeader: [] - {} requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/RpcRequisicao' } example: jsonrpc: '2.0' id: 1 method: tools/list responses: '200': description: | Resposta JSON-RPC. Inclui os casos de erro de negócio (`result.isError = true`) e de método desconhecido (`error.code = -32601`). content: application/json: schema: { $ref: '#/components/schemas/RpcResposta' } examples: semPermissao: value: jsonrpc: '2.0' id: 7 result: content: - type: text text: sua chave não tem acesso ao módulo "fiscal" · peça ao administrador em Configurações isError: true '202': description: Notificação aceita. Corpo vazio. '401': description: Chave inválida ou revogada. content: application/json: schema: { $ref: '#/components/schemas/RpcErro' } example: jsonrpc: '2.0' id: 1 error: code: -32001 message: token inválido ou revogado '429': description: Teto de 120 chamadas/min por chave. content: application/json: schema: { $ref: '#/components/schemas/RpcErro' } example: jsonrpc: '2.0' id: 1 error: code: -32002 message: muitas chamadas seguidas — espere 37s e tente de novo '500': description: Exceção não tratada (`code -32603`, mensagem truncada em 300 chars). content: application/json: schema: { $ref: '#/components/schemas/RpcErro' } get: tags: [MCP JSON-RPC] operationId: mcpOperadorGet summary: Não suportado security: [{}] responses: '405': description: "Corpo vazio, com header `Allow: POST`." headers: Allow: schema: { type: string, const: POST } /api/cliente/mcp/{chave}: post: tags: [MCP JSON-RPC, API da empresa (chave por empresa)] operationId: mcpEmpresa summary: Fachada JSON-RPC da API da empresa, com a chave no path description: | Mesmo miolo de `/api/cliente/v1` (mesma tabela, mesmo escopo, mesma auditoria). A chave vai no path porque o app do Claude não tem campo de header ao adicionar um conector personalizado. Também existe `POST /api/cliente/mcp`, que lê a chave do header `Authorization: Bearer` ou `X-Api-Key`. Protocolo `2025-06-18`; também aceita `2024-11-05`, `2025-03-26` e `2026-07-28`. Ferramentas de escrita só **aparecem** em `tools/list` se a chave for `completo`. Corpo limitado a 1 MB. security: [{}] parameters: - name: chave in: path required: true description: A chave da empresa (prefixo `ctb_`). Trate como segredo — ela vai na URL. schema: { type: string } requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/RpcRequisicao' } responses: '200': description: Resposta JSON-RPC. content: application/json: schema: { $ref: '#/components/schemas/RpcResposta' } '202': description: Notificação aceita. Corpo vazio. '401': description: Chave inválida ou revogada (`code -32001`). content: application/json: schema: { $ref: '#/components/schemas/RpcErro' } '500': description: Exceção não tratada (`code -32603`). content: application/json: schema: { $ref: '#/components/schemas/RpcErro' } get: tags: [MCP JSON-RPC] operationId: mcpEmpresaGet summary: Não suportado security: [{}] parameters: - name: chave in: path required: true schema: { type: string } responses: '405': description: "Corpo vazio, com header `Allow: POST`." headers: Allow: schema: { type: string, const: POST } ############################################################################ # CHAVES ############################################################################ /api/mcp/tokens: get: tags: [Chaves e auditoria] operationId: listarChavesOperador summary: Lista as chaves de operador description: | Administrador vê todas as chaves do escritório e a lista de operadores; operador comum vê só as próprias e recebe `operadores: []`. O hash da chave nunca é devolvido. security: - JwtOperador: [] - ChaveInterna: [] responses: '200': description: Chaves. content: application/json: schema: type: object properties: ok: { const: true } sou_admin: { type: boolean } meu_email: { type: string } tokens: { type: array, items: { type: object, additionalProperties: true } } operadores: { type: array, items: { type: object, additionalProperties: true } } modulos: { type: array, items: { $ref: '#/components/schemas/Modulo' } } modulos_permitidos: { type: array, items: { type: string } } additionalProperties: true '401': { $ref: '#/components/responses/NaoAutenticadoOperador' } post: tags: [Chaves e auditoria] operationId: criarChaveOperador summary: Emite uma chave de operador (`cnt_`) description: | **É assim que se emite a credencial de um integrador externo.** Quem pode: qualquer operador autenticado emite chave **para si**; só administrador emite **em nome de terceiro** e só administrador escolhe módulos fora das permissões da pessoa. Para não-admin, o `operador_email` do corpo é ignorado e substituído pelo e-mail de quem chama. O e-mail precisa existir em `operators` — na prática, crie um operador de serviço (ex.: `integracao-marcom@...`) e emita a chave no nome dele. A chave herda essa identidade: é o nome dele que aparece na auditoria e nos campos de "revisado por". Módulo inválido é filtrado em silêncio; se sobrar zero, é 400. O valor cru da chave sai **uma única vez**, nesta resposta. O banco guarda só o sha256. security: - JwtOperador: [] - ChaveInterna: [] requestBody: required: true content: application/json: schema: type: object properties: operador_email: type: string format: email description: Ignorado para quem não é administrador. escopo: type: string enum: [leitura, completo] default: leitura modulos: type: array minItems: 1 items: type: string enum: [contabil, fiscal, folha, empresas, incubadora, financeiro] rotulo: type: string description: 'Nome amigável. Padrão: `MCP · `.' expira_em_dias: type: integer description: '0 ou ausente = não expira.' required: [modulos] additionalProperties: false example: operador_email: integracao-marcom@escritorio.com.br escopo: completo modulos: [empresas, financeiro, fiscal] rotulo: Integração MARCOM expira_em_dias: 365 responses: '200': description: Chave criada. **O campo `token` não é mostrado de novo.** content: application/json: schema: type: object properties: ok: { const: true } token: type: string description: '`cnt_` + 64 hex = 68 caracteres.' registro: type: object properties: id: { type: string, format: uuid } operador_email: { type: string } rotulo: { type: [string, 'null'] } escopo: { type: string, enum: [leitura, completo] } modulos: { type: array, items: { type: string } } expira_em: { type: [string, 'null'], format: date-time } criado_em: { type: string, format: date-time } additionalProperties: true required: [ok, token] '400': description: | E-mail inválido, escopo fora de `leitura|completo`, nenhum módulo válido, ou operador inexistente. content: application/json: schema: { $ref: '#/components/schemas/Erro' } examples: semModulo: value: ok: false error: marque pelo menos um módulo · chave sem módulo não enxerga ferramenta nenhuma semOperador: value: ok: false error: não existe operador com o e-mail integracao-marcom@escritorio.com.br '401': { $ref: '#/components/responses/NaoAutenticadoOperador' } '403': description: Operador comum pedindo módulo que ele mesmo não tem. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: ok: false error: 'você não tem acesso a: fiscal, folha · peça a um administrador' /api/mcp/tokens/{id}: patch: tags: [Chaves e auditoria] operationId: alterarChaveOperador summary: Revoga, reativa ou troca os módulos de uma chave description: | Revogação vale **na hora**: a validação relê `ativo` a cada chamada, sem cache. Não existe `DELETE` — a linha nunca é apagada. Operador comum só mexe na própria chave e só para revogar; trocar módulos é de administrador. security: - JwtOperador: [] - ChaveInterna: [] parameters: - name: id in: path required: true schema: { type: string, format: uuid } requestBody: required: true content: application/json: schema: type: object properties: ativo: { type: boolean } modulos: type: array minItems: 1 items: type: string enum: [contabil, fiscal, folha, empresas, incubadora, financeiro] additionalProperties: false example: ativo: false responses: '200': description: 'Devolve `{ok:true}` mais os campos alterados.' content: application/json: schema: type: object properties: ok: { const: true } additionalProperties: true example: ok: true ativo: false '400': description: Nada para alterar, ou lista de módulos vazia depois do filtro. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: { ok: false, error: nada pra alterar } '401': { $ref: '#/components/responses/NaoAutenticadoOperador' } '403': description: Chave de outra pessoa, ou não-admin trocando módulos. content: application/json: schema: { $ref: '#/components/schemas/Erro' } examples: naoEhSua: value: { ok: false, error: essa chave não é sua } soAdmin: value: ok: false error: só administrador muda os módulos · você pode revogar e gerar outra /api/mcp/auditoria: get: tags: [Chaves e auditoria] operationId: auditoriaChaveOperador summary: Trilha de uso das chaves description: | Lê a tabela `mcp_auditoria`. Administrador vê tudo; operador comum vê só o próprio uso. Toda chamada REST (`sessao = "rest"`) e todo `tools/call` são gravados. NÃO CONFIRMADO: shape exato da resposta. security: - JwtOperador: [] - ChaveInterna: [] parameters: - name: dias in: query description: Janela em dias, entre 1 e 90. schema: { type: integer, minimum: 1, maximum: 90 } responses: '200': description: Registros de uso. content: application/json: schema: { type: object, additionalProperties: true } '401': { $ref: '#/components/responses/NaoAutenticadoOperador' } ############################################################################ # EMPRESAS — rotas cruas ############################################################################ /api/clients: get: tags: [Empresas (rotas cruas)] operationId: listarClients summary: Lista as empresas do escritório (objeto curado) description: | Autenticação obrigatória e explícita: JWT de operador do Supabase, ou a chave interna do processo. **O header `X-Operator-Email` não é credencial** — é só auditoria. Comportamentos que surpreendem: - **descarta toda linha cujo CNPJ não tenha exatamente 14 dígitos** — ou seja, lead sem CNPJ não aparece aqui; - `status` e `limit` são aplicados **em memória**, depois de ler a tabela; - `total` é contado **antes** do corte de `limit`; - a resposta tem cache HTTP de 30 s; - **não devolve `monthly_value`**. security: - JwtOperador: [] - ChaveInterna: [] parameters: - name: status in: query description: Filtro exato de status. schema: type: string enum: [lead, fechando_contrato, active, paused, cancelled, archived] - name: limit in: query description: Máximo de linhas. Padrão 500, teto 2000. schema: { type: integer, default: 500, maximum: 2000 } responses: '200': description: Empresas. content: application/json: schema: type: object properties: ok: { const: true } total: { type: integer, description: Contagem antes do corte de `limit`. } clients: type: array items: { $ref: '#/components/schemas/ClienteCurado' } required: [ok, total, clients] '401': { $ref: '#/components/responses/NaoAutenticadoOperador' } /api/clients/{id}: get: tags: [Empresas (rotas cruas)] operationId: obterClient summary: Registro completo de uma empresa description: | Devolve **a linha crua hidratada**, sem envelope `{ok:true}`: o conteúdo do JSONB `extra_data` é espalhado no topo do objeto e as colunas reais vencem em caso de nome repetido. `extra_data` continua presente também. ⚠️ Esta rota **não tem porteiro próprio** (ao contrário de `GET /api/clients`): ela depende inteiramente do middleware global, cujo modo estrito é controlado por variável de ambiente. Ver `NÃO CONFIRMADO` em `API.md`. Aceita UUID ou o `legacy_id` do sistema antigo. security: - JwtOperador: [] - ChaveInterna: [] parameters: - $ref: '#/components/parameters/ClienteId' responses: '200': description: | Objeto da empresa. Campos variam: parte é coluna, parte vem de `extra_data`. content: application/json: schema: type: object properties: id: { type: string } extra_data: { type: object, additionalProperties: true } additionalProperties: true '404': description: Não encontrada. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: { ok: false, error: cliente não encontrado } '500': $ref: '#/components/responses/ErroInterno' /api/clients/{id}/status: post: tags: [Empresas (rotas cruas)] operationId: trocarStatusClient summary: Troca o status da empresa description: | Também zera `cancelled_at` e `cancellation_reason` quando o novo status é diferente de `cancelled`. Resolve o `:id` por UUID e, se não achar, por `legacy_id`. security: - JwtOperador: [] - ChaveInterna: [] parameters: - $ref: '#/components/parameters/ClienteId' requestBody: required: true content: application/json: schema: type: object properties: status: { $ref: '#/components/schemas/StatusCliente' } required: [status] additionalProperties: false responses: '200': description: Status trocado. content: application/json: schema: type: object properties: ok: { const: true } client: type: object properties: id: { type: string } name: { type: string } status: { type: string } required: [ok, client] '400': description: Status fora da lista. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: ok: false error: 'status inválido (use: lead, fechando_contrato, active, paused, cancelled, archived)' '401': { $ref: '#/components/responses/NaoAutenticadoOperador' } /api/clients/{id}/cancel: post: tags: [Empresas (rotas cruas)] operationId: cancelarClient summary: Marca a empresa como cancelada (mas **não** muda o status) description: | ⚠️ **Armadilha confirmada:** esta rota grava `cancelled_at` e `cancellation_reason`, mas **não altera o campo `status`**. A empresa continua aparecendo como `active` nas listagens. Para tirar da operação, chame também `POST /api/clients/{id}/status` com `cancelled`. Resolve o `:id` **só por UUID** — diferente das rotas irmãs, aqui não há fallback para `legacy_id`. security: - JwtOperador: [] - ChaveInterna: [] parameters: - $ref: '#/components/parameters/ClienteId' requestBody: required: false content: application/json: schema: type: object properties: reason: { type: string, maxLength: 500 } sendDocuments: { type: boolean } additionalProperties: false responses: '200': description: Cancelamento registrado. content: application/json: schema: type: object properties: ok: { const: true } client: type: object properties: id: { type: string } name: { type: string } email: { type: string } cnpj: { type: string } docs_scheduled: { type: boolean } grace_days: { type: integer, const: 30 } additionalProperties: true '401': { $ref: '#/components/responses/NaoAutenticadoOperador' } /api/clients/{id}/reactivate: post: tags: [Empresas (rotas cruas)] operationId: reativarClient summary: Reativa a empresa description: | Corpo vazio. Seta `status = active` e zera `cancelled_at`, `cancellation_reason` e `documents_sent_at`. Resolve por UUID e, se não achar, por `legacy_id`. security: - JwtOperador: [] - ChaveInterna: [] parameters: - $ref: '#/components/parameters/ClienteId' responses: '200': description: Reativada. content: application/json: schema: type: object properties: ok: { const: true } required: [ok] '401': { $ref: '#/components/responses/NaoAutenticadoOperador' } /api/clients/{id}/atualizar-receita: post: tags: [Empresas (rotas cruas)] operationId: atualizarReceitaClient summary: Sincroniza o cadastro com a Receita (BrasilAPI) description: | **Corpo vazio.** Nenhum campo é aceito do chamador — tudo vem da Receita. É a única forma suportada de "atualizar cadastro" por HTTP. Escreve 18 campos: `name, nome_fantasia, cnae, cnae_fiscal, cnae_fiscal_descricao, cnaes_secundarios, natureza_juridica, porte, capital_social, situacao_cadastral, data_situacao_cadastral, motivo_situacao_cadastral, data_inicio_atividade, phone, email, qsa, address, municipio, uf, last_official_sync_at`. Regras: 1. **nunca sobrescreve telefone/e-mail que já existem no painel** — a diferença sai em `divergencia_contato`; 2. campo vazio na Receita não apaga o que existe; 3. para MEI sem quadro societário na BrasilAPI, deriva um sócio "Titular MEI" 100%. A consulta à Receita é gratuita (BrasilAPI) — não gera custo de SERPRO. security: - JwtOperador: [] - ChaveInterna: [] parameters: - $ref: '#/components/parameters/ClienteId' responses: '200': description: Cadastro atualizado. content: application/json: schema: type: object properties: ok: { const: true } empresa: { type: object, additionalProperties: true } campos_alterados: { type: array, items: { type: string } } alteracoes: type: object additionalProperties: type: object properties: de: {} para: {} divergencia_contato: type: object additionalProperties: true description: Presente só quando o telefone/e-mail da Receita difere do painel. additionalProperties: true '400': description: CNPJ inválido no cadastro. content: application/json: schema: { $ref: '#/components/schemas/Erro' } '404': description: Empresa não encontrada. content: application/json: schema: { $ref: '#/components/schemas/Erro' } '424': description: | A Receita não devolveu o CNPJ. **424, não 5xx** — o proxy do ambiente sequestra respostas 5xx do backend, então falha de dependência externa é sinalizada assim. content: application/json: schema: { $ref: '#/components/schemas/Erro' } /api/clients/{id}/caracteristicas-fiscais: post: tags: [Empresas (rotas cruas)] operationId: caracteristicasFiscaisClient summary: Atualiza as características fiscais da empresa description: | Allowlist estrita: só os campos abaixo são aceitos, qualquer outro é ignorado. Se nenhum campo válido vier, é 400. Carimba quem alterou e quando, e grava log de auditoria. security: - JwtOperador: [] - ChaveInterna: [] parameters: - $ref: '#/components/parameters/ClienteId' requestBody: required: true content: application/json: schema: type: object properties: contribuinte_icms: { type: boolean } contribuinte_iss: { type: boolean } contribuinte_ipi: { type: boolean } tem_folha_clt: { type: boolean } exporta: { type: boolean } cprb: { type: boolean } iss_retido_padrao: { type: boolean } iss_fora_municipio_padrao: { type: boolean } escritorio_contabil_iss_fixo: { type: boolean } com_st: { type: boolean } atividades: type: array items: { type: string } anexo: { $ref: '#/components/schemas/AnexoSimples' } additionalProperties: false responses: '200': description: Campos atualizados. content: application/json: schema: type: object properties: ok: { const: true } updated: { type: array, items: { type: string } } required: [ok, updated] '400': description: Nenhum campo válido no corpo, ou `anexo` fora de I..V. content: application/json: schema: { $ref: '#/components/schemas/Erro' } '401': { $ref: '#/components/responses/NaoAutenticadoOperador' } /api/clients/{id}/sem-contabilidade: post: tags: [Empresas (rotas cruas)] operationId: semContabilidadeClient summary: Marca a empresa como "sem contabilidade" description: | Grava em `extra_data.sem_contabilidade`. NÃO CONFIRMADO: shape da resposta. security: - JwtOperador: [] - ChaveInterna: [] parameters: - $ref: '#/components/parameters/ClienteId' requestBody: required: false content: application/json: schema: type: object properties: value: { type: boolean, default: true } motivo: { type: string } additionalProperties: false responses: '200': description: 'NÃO CONFIRMADO: shape da resposta.' content: application/json: schema: { type: object, additionalProperties: true } /api/clients/{id}/procuracao: post: tags: [Empresas (rotas cruas)] operationId: procuracaoClient summary: Registra a procuração eletrônica da empresa description: 'NÃO CONFIRMADO: shape da resposta.' security: - JwtOperador: [] - ChaveInterna: [] parameters: - $ref: '#/components/parameters/ClienteId' requestBody: required: true content: application/json: schema: type: object properties: outorgada: { type: boolean } motivo: { type: string } validade: { type: string, description: 'NÃO CONFIRMADO: formato (provavelmente AAAA-MM-DD).' } required: [outorgada] additionalProperties: false responses: '200': description: 'NÃO CONFIRMADO: shape da resposta.' content: application/json: schema: { type: object, additionalProperties: true } ############################################################################ # CRIAÇÃO DE EMPRESA ############################################################################ /api/admin/clients/import-csv: post: tags: [Criação de empresa] operationId: importarClientesCsv summary: Cria empresas em lote a partir de um CSV description: | **Um dos dois únicos caminhos HTTP que criam empresa.** Só administrador. Corpo até 5 MB. Tem freio por operador (429). Parsing: separador detectado automaticamente (`;` se houver, senão `,`); cabeçalho normalizado para minúsculas com `[^a-z0-9_]` virando `_`; colunas achadas por `includes`: | Campo | Aceita cabeçalho contendo | Obrigatória | |---|---|---| | CNPJ | `cnpj`, `cpfcnpj`, `cpf_cnpj` | **sim** (400 se ausente) | | nome | `nome`, `razao_social`, `razaosocial`, `razao` | não | | regime | `regime`, `regime_tributario` | não | | e-mail | `email`, `e_mail` | não | | telefone | `phone`, `celular`, `whatsapp`, `telefone` | não | ⚠️ **A validação de CNPJ aqui é só contagem de 14 dígitos — o dígito verificador não é conferido.** ⚠️ O valor de `regime` é gravado **cru**. O banco tem um CHECK com quatro valores (`MEI`, `Simples Nacional`, `Lucro Presumido`, `Lucro Real`); qualquer coisa fora disso viola a constraint, o erro é engolido por linha e a linha vira `{status:"erro"}` — com **HTTP 200** no envelope. ⚠️ **Não devolve o `id` das empresas criadas.** CNPJ que já existe é pulado (`status: "duplicado"`), sem erro e sem atualizar nada. O CSV também é deduplicado contra si mesmo. security: - JwtOperador: [] - ChaveInterna: [] requestBody: required: true content: application/json: schema: type: object properties: csv_text: type: string description: Conteúdo do CSV, com cabeçalho. Mínimo 2 linhas. dryRun: type: boolean description: Só simula, não grava. enrich: type: boolean description: Consulta a BrasilAPI por linha (timeout de 5 s). required: [csv_text] additionalProperties: false example: csv_text: "cnpj;razao_social;regime;email\n12345678000190;AGENCIA MAXIMUS LTDA;Simples Nacional;financeiro@maximus.com.br" dryRun: true responses: '200': description: Resultado do lote. **Sempre 200, mesmo com linhas em erro.** content: application/json: schema: type: object properties: ok: { const: true } dryRun: { type: boolean } total_linhas: { type: integer, description: Linhas menos o cabeçalho. } criados: { type: integer } duplicados: { type: integer } invalidos: { type: integer } erros: { type: integer } resultados: type: array items: { $ref: '#/components/schemas/LinhaImportCsv' } required: [ok, total_linhas, criados, duplicados, invalidos, erros, resultados] example: ok: true dryRun: false total_linhas: 12 criados: 9 duplicados: 2 invalidos: 1 erros: 0 resultados: - linha: 2 cnpj: '12345678000190' nome: AGENCIA MAXIMUS LTDA status: criado - linha: 3 cnpj: '999' status: invalido error: cnpj inválido '400': description: '`csv_text` ausente, menos de 2 linhas, ou sem coluna de CNPJ.' content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: { ok: false, error: csv_text obrigatorio } '403': description: Não é administrador. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: ok: false error: apenas administradores podem importar clientes em lote '429': description: Freio por operador. '500': $ref: '#/components/responses/ErroInterno' /api/integra-cert/upload-or-create: post: tags: [Criação de empresa] operationId: criarClientePorCertificado summary: Cria (ou reusa) a empresa a partir de um certificado A1 description: | **O outro caminho HTTP de criação.** O CNPJ e a razão social são extraídos do subject ICP-Brasil do arquivo `.pfx` (`CN=RAZAO:CNPJ`) — **não vêm do corpo**. É o único caminho com semântica de *get-or-create*: se já existir empresa com aquele CNPJ, ela é reusada e a resposta traz `createdNew: false`. Cria com `regime: ""` (string vazia) — que **não** é um dos quatro valores aceitos pelo CHECK de `regime` no banco. Ver `NÃO CONFIRMADO` em `API.md`. Certificado vencido é rejeitado com 400. security: - JwtOperador: [] - ChaveInterna: [] parameters: - name: X-Operator-Uid in: header required: true description: | UID do operador que fica como dono da empresa criada. **Obrigatório** — sem ele é 400. (Não é credencial; a autenticação é a do middleware.) schema: { type: string } requestBody: required: true content: application/json: schema: type: object properties: pfx_base64: { type: string, description: O arquivo .pfx em base64. } password: { type: string, description: Senha do certificado. } required: [pfx_base64, password] additionalProperties: false responses: '200': description: Empresa criada ou reusada. content: application/json: schema: type: object properties: ok: { const: true } clientId: { type: string, description: UUID da empresa (nova ou existente). } createdNew: { type: boolean } cnpj: { type: string, description: 14 dígitos, extraído do certificado. } razao: { type: string, description: CN do subject. } existingName: type: [string, 'null'] description: Nome da empresa reusada; `null` quando criou. valid_to: { type: string, format: date-time } days_until_expiry: { type: integer } required: [ok, clientId, createdNew, cnpj, razao] example: ok: true clientId: 6f1f0f5a-1c3e-4a2b-9f0d-6b6a2f0f9c11 createdNew: true cnpj: '12345678000190' razao: AGENCIA MAXIMUS LTDA existingName: null valid_to: '2027-01-01T00:00:00.000Z' days_until_expiry: 178 '400': description: | `pfx_base64`/`password` ausentes, `X-Operator-Uid` ausente, base64 inválido, senha errada, CNPJ não extraível, ou **certificado vencido** (que devolve `{expired:true, days_until_expiry, valid_to}`). content: application/json: schema: type: object additionalProperties: true example: expired: true days_until_expiry: -12 valid_to: '2026-08-14T00:00:00.000Z' '500': description: Falha ao gravar. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: { ok: false, error: 'Falha DB Admin: ...' } ############################################################################ # CONTRATO ############################################################################ /api/portal-api/{cnpj}/contrato: get: tags: [Contrato e honorário, portal-api (server↔server)] operationId: contratoPorCnpj summary: Contrato assinado mais recente da empresa description: | **Esta é a fonte da verdade do valor mensal contratado.** O valor mora dentro do JSONB `extra_data` do contrato assinado, não em coluna — e não em `clients.monthly_value`. Implementação: pega os 50 contratos `signed` mais recentes (global) e filtra em memória pelo `client_id` ou pelo CNPJ gravado no contrato. Se a empresa não tiver contrato assinado, devolve `tem_contrato: false` com HTTP 200. security: - PortalApiKeyHeader: [] - PortalApiKeyBearer: [] parameters: - $ref: '#/components/parameters/Cnpj' responses: '200': description: Contrato (ou a informação de que não há). content: application/json: schema: oneOf: - type: object properties: ok: { const: true } tem_contrato: { const: true } contrato: { $ref: '#/components/schemas/ContratoResumo' } required: [ok, tem_contrato, contrato] - type: object properties: ok: { const: true } tem_contrato: { const: false } required: [ok, tem_contrato] examples: comContrato: value: ok: true tem_contrato: true contrato: valor_mensal: 347 vigencia: Indeterminado assinado_em: '2026-03-14T18:22:05.000Z' semContrato: value: ok: true tem_contrato: false '400': description: CNPJ sem 14 dígitos. content: application/json: schema: { $ref: '#/components/schemas/Erro' } '401': { $ref: '#/components/responses/PortalNaoAutenticado' } /api/public-contracts/{token}: get: tags: [Contrato e honorário] operationId: contratoPublicoPorToken summary: Contrato pela página pública de assinatura description: | **Rota pública, sem autenticação** — é o que alimenta a tela `/sign/{token}`. O token na URL é o segredo. Devolve apenas a allowlist de campos públicos, achatados no topo do objeto (no banco, eles vivem dentro do JSONB `extra_data`). security: [{}] parameters: - name: token in: path required: true schema: { type: string } responses: '200': description: Contrato público. content: application/json: schema: type: object properties: client_name: { type: [string, 'null'] } client_cnpj: { type: [string, 'null'] } type: { type: [string, 'null'] } monthly_value: { type: [number, 'null'] } due_date: type: [string, 'null'] description: | Contrato mensal traz a **string literal** `Indeterminado`; serviço extraordinário traz uma data `AAAA-MM-DD`. term_text: { type: [string, 'null'] } additionalProperties: true /api/contracts/conferencia-honorario: get: tags: [Contrato e honorário] operationId: conferenciaHonorario summary: Compara o valor do contrato com a assinatura ativa no Asaas description: | Sem parâmetros. Percorre os contratos assinados e casa com a assinatura `ACTIVE` do Asaas pelo `asaas_id` da empresa. Tolerância de 1 centavo. security: - JwtOperador: [] - ChaveInterna: [] responses: '200': description: Conferência. content: application/json: schema: type: object properties: ok: { const: true } total: { type: integer } divergentes: { type: integer } sem_assinatura: { type: integer } itens: type: array items: type: object properties: valor_combinado: { type: [number, 'null'] } valor_cobrado: { type: [number, 'null'] } additionalProperties: true additionalProperties: true /api/contratos/honorario-reconciliacao: get: tags: [Contrato e honorário] operationId: honorarioReconciliacao summary: Recalcula o honorário esperado e compara com o Asaas description: | Sem parâmetros; as competências olhadas são fixas (mês passado e o anterior). **Sempre exige autenticação**, mesmo quando o modo brando estiver ligado. A regra do valor esperado, confirmada no código: base = honorário fixo, se marcado; senão a faixa por faturamento esperado = desconto( base + (nº de funcionários CLT × R$ 50) ) Faixa por faturamento mensal: MEI R$ 157; ME até R$ 50.000 → R$ 347; acima disso, R$ 347 + R$ 100 por faixa iniciada de R$ 50.000. security: - JwtOperador: [] - ChaveInterna: [] responses: '200': description: Reconciliação por empresa. content: application/json: schema: type: object properties: itens: type: array items: type: object properties: faixa_base: { type: number } add_funcionarios: { type: number } esperado_bruto: { type: number } desconto: { type: number } honorario_fixo: { type: boolean } esperado: { type: number } cobrado: { type: [number, 'null'] } diff: { type: number } status: type: string enum: [ok, cobra_menos, cobra_mais, sem_faturamento, sem_assinatura] additionalProperties: true additionalProperties: true '401': { $ref: '#/components/responses/NaoAutenticadoOperador' } ############################################################################ # FATURAMENTO ############################################################################ /api/apuracao/{cnpj}/faturamento-periodo: get: tags: [Faturamento e apuração] operationId: faturamentoPeriodo summary: Faturamento de uma empresa em um período livre description: | A rota mais direta para "quanto essa empresa faturou entre X e Y". Teto de **60 meses** por chamada. `meses[].fonte` diz de onde o número veio, em ordem de prioridade: o que foi **declarado** no PGDAS, depois o oficial do SERPRO, depois os portais de NFS-e, depois o manual, depois o apurado no painel. **Mês sem registro não é zero** — ele sai em `sem_registro`, e não entra em `meses`. security: - JwtOperador: [] - ChaveInterna: [] parameters: - $ref: '#/components/parameters/Cnpj' - name: de in: query required: true description: Competência inicial. schema: { $ref: '#/components/schemas/Competencia' } - name: ate in: query required: true description: Competência final. schema: { $ref: '#/components/schemas/Competencia' } responses: '200': description: Faturamento do período. content: application/json: schema: type: object properties: ok: { const: true } cnpj: { type: string } de: { $ref: '#/components/schemas/Competencia' } ate: { $ref: '#/components/schemas/Competencia' } meses: type: array items: type: object properties: competencia: { $ref: '#/components/schemas/Competencia' } total: { type: number, description: Reais. Sem arredondamento forçado. } fonte: type: string enum: [manual, painel, serpro, pdf, blumenau, nacional, sem_apuracao, sem_dados] fonte_label: { type: string } puxado_em: { type: [string, 'null'] } additionalProperties: true total: type: number description: Soma dos meses, arredondada em 2 casas. qtd_meses: { type: integer } sem_registro: type: array items: { $ref: '#/components/schemas/Competencia' } description: Competências sem apuração no período. **Não são zero.** required: [ok, cnpj, de, ate, meses, total, qtd_meses, sem_registro] example: ok: true cnpj: '12345678000190' de: '2026-01' ate: '2026-06' meses: - competencia: '2026-01' total: 41230.55 fonte: painel fonte_label: Declarado no PGDAS puxado_em: '2026-02-11T13:04:00.000Z' total: 41230.55 qtd_meses: 1 sem_registro: ['2026-02', '2026-03', '2026-04', '2026-05', '2026-06'] '400': description: | CNPJ inválido, `de`/`ate` fora do formato `AAAA-MM`, `de` depois de `ate`, ou período maior que 60 meses. content: application/json: schema: type: object properties: error: { type: string } examples: formato: value: { error: de/ate no formato YYYY-MM } periodo: value: { error: período maior que 60 meses } /api/apuracao/{cnpj}/resumo-anual: get: tags: [Faturamento e apuração] operationId: resumoAnualPorCnpj summary: Faturamento mês a mês de uma empresa em um ano description: | Sempre 12 meses. Soma as notas capturadas com os lançamentos avulsos. security: - JwtOperador: [] - ChaveInterna: [] parameters: - $ref: '#/components/parameters/Cnpj' - name: ano in: query description: Padrão, o ano corrente. schema: { type: integer, minimum: 2010, maximum: 2099 } responses: '200': description: Resumo do ano. content: application/json: schema: type: object properties: ok: { const: true } ano: { type: integer } faturamento_anual: { type: number } meses: type: array items: type: object properties: competencia: { $ref: '#/components/schemas/Competencia' } total: { type: number } qtd: { type: integer } sincronizado: { type: boolean } capturado_em: { type: [string, 'null'] } additionalProperties: true required: [ok, ano, faturamento_anual, meses] '400': description: Ano fora da faixa 2010–2099. content: application/json: schema: type: object properties: error: { type: string } /api/apuracao/resumo-anual-batch: get: tags: [Faturamento e apuração] operationId: resumoAnualBatch summary: Faturamento anual de todas as empresas description: | Agrega por CNPJ dentro do ano. ⚠️ **Dois avisos.** 1. A soma daqui usa uma regra **diferente** da usada em `GET /api/apuracao/{cnpj}/resumo-anual` — os totais podem não bater entre as duas rotas. 2. A consulta interna roda **sem limite**. Com muitas empresas × 12 meses ela passa do teto padrão do banco e **trunca em silêncio**. Ver `NÃO CONFIRMADO` em `API.md`. security: - JwtOperador: [] - ChaveInterna: [] parameters: - name: ano in: query schema: { type: integer, minimum: 2010, maximum: 2099 } responses: '200': description: Faturamento anual por empresa. content: application/json: schema: type: object properties: ok: { const: true } ano: { type: integer } total: { type: integer } items: type: array items: type: object properties: cnpj: { type: string } faturamento_anual: { type: number } qtd_meses_com_dados: { type: integer } additionalProperties: true required: [ok, ano, total, items] /api/apuracao/list: get: tags: [Faturamento e apuração] operationId: listarApuracoes summary: Todas as apurações de uma competência description: | Uma competência, todas as empresas do regime pedido. Devolve o documento inteiro de cada apuração — **não soma nada**. Respeita a migração de regime dentro da competência. ⚠️ Roda sem limite explícito; sujeito ao teto padrão do banco, que trunca em silêncio. security: - JwtOperador: [] - ChaveInterna: [] parameters: - name: competencia in: query required: true schema: { $ref: '#/components/schemas/Competencia' } - name: regime in: query description: Padrão `sn`. schema: type: string enum: [sn, mei, lp, lr] default: sn responses: '200': description: Apurações da competência. content: application/json: schema: type: object properties: items: type: array items: type: object properties: id: { type: string } additionalProperties: true total: { type: integer } required: [items, total] '400': description: Competência fora do formato. content: application/json: schema: type: object properties: error: { type: string } example: { error: competencia inválida · use YYYY-MM } /api/portal-api/{cnpj}/faturamento: get: tags: [Faturamento e apuração, portal-api (server↔server)] operationId: faturamentoPortalApi summary: Faturamento anual da empresa, separando nota de avulso description: | É a rota que o painel do cliente usa. Separa, mês a mês, o que veio de nota fiscal do que foi lançado como avulso. security: - PortalApiKeyHeader: [] - PortalApiKeyBearer: [] parameters: - $ref: '#/components/parameters/Cnpj' - name: ano in: query description: 'Padrão, o ano corrente. NÃO CONFIRMADO: não há validação de faixa aqui.' schema: { type: integer } responses: '200': description: Faturamento do ano. content: application/json: schema: type: object properties: ok: { const: true } ano: { type: integer } regime: { type: [string, 'null'] } is_mei: { type: boolean } limite: { type: [number, 'null'], description: Teto do regime, em reais. } faturamento_anual: { type: number } meses: type: array items: type: object properties: competencia: { $ref: '#/components/schemas/Competencia' } total: { type: number } notas: { type: number, description: 'total menos avulso, nunca negativo.' } avulso: { type: number } qtd: { type: integer } avulsos: type: array items: { $ref: '#/components/schemas/AvulsoResumo' } additionalProperties: true additionalProperties: true '401': { $ref: '#/components/responses/PortalNaoAutenticado' } /api/portal-api/{cnpj}/apuracao: get: tags: [Faturamento e apuração, portal-api (server↔server)] operationId: apuracaoPortalApi summary: Apuração da empresa no ano description: 'NÃO CONFIRMADO: shape da resposta.' security: - PortalApiKeyHeader: [] - PortalApiKeyBearer: [] parameters: - $ref: '#/components/parameters/Cnpj' - name: ano in: query schema: { type: integer, minimum: 2010, maximum: 2099 } responses: '200': description: 'NÃO CONFIRMADO: shape da resposta.' content: application/json: schema: { type: object, additionalProperties: true } '400': description: Ano fora da faixa 2010–2099. content: application/json: schema: { $ref: '#/components/schemas/Erro' } '401': { $ref: '#/components/responses/PortalNaoAutenticado' } /api/portal-api/{cnpj}/notas: get: tags: [Faturamento e apuração, portal-api (server↔server)] operationId: notasPortalApi summary: Notas fiscais emitidas e recebidas da empresa description: | Aceita **só** `ano`. Teto interno fixo de 500 notas, **sem paginação e sem contagem total**. ⚠️ Este path está registrado **duas vezes** no servidor. A segunda versão — a única que teria `competencia`, `limit` e `total_valor` — é inalcançável (código morto). Não conte com esses parâmetros: eles não existem na prática. security: - PortalApiKeyHeader: [] - PortalApiKeyBearer: [] parameters: - $ref: '#/components/parameters/Cnpj' - name: ano in: query schema: { type: integer, minimum: 2009 } responses: '200': description: Notas. content: application/json: schema: type: object properties: ok: { const: true } notas: type: array items: type: object properties: valor: { type: number, description: Reais. } additionalProperties: true notas_recebidas: type: array items: { type: object, additionalProperties: true } config: { type: object, additionalProperties: true } emissao_bloqueio: {} atualizado_em: { type: [string, 'null'] } additionalProperties: true '401': { $ref: '#/components/responses/PortalNaoAutenticado' } /api/portal-api/{cnpj}/faturamento/avulso: post: tags: [Faturamento e apuração, portal-api (server↔server)] operationId: lancarAvulsoPortalApi summary: Lança faturamento sem nota para a empresa description: | Encaminha para o lançamento avulso interno, marcando a origem como o painel do cliente. security: - PortalApiKeyHeader: [] - PortalApiKeyBearer: [] parameters: - $ref: '#/components/parameters/Cnpj' requestBody: required: true content: application/json: schema: type: object properties: competencia: { $ref: '#/components/schemas/Competencia' } valor: { type: number, exclusiveMinimum: 0, description: Reais. } descricao: { type: string } required: [competencia, valor] additionalProperties: false responses: '200': description: 'NÃO CONFIRMADO: shape da resposta.' content: application/json: schema: { type: object, additionalProperties: true } '400': description: Competência fora do formato, valor não positivo, ou mês futuro. content: application/json: schema: { $ref: '#/components/schemas/Erro' } examples: competencia: value: { ok: false, error: competencia YYYY-MM obrigatória } valor: value: { ok: false, error: valor deve ser maior que zero } futuro: value: { ok: false, error: não dá pra lançar faturamento de um mês futuro } '401': { $ref: '#/components/responses/PortalNaoAutenticado' } /api/avulsos/{alvo}: description: | ⚠️ O mesmo caminho serve a dois identificadores diferentes, conforme o verbo: **GET** espera o **CNPJ** da empresa; **PUT** e **DELETE** esperam o **id do lançamento**. Não é um recurso REST coerente — é assim no servidor. get: tags: [Faturamento e apuração] operationId: listarAvulsos summary: Lançamentos avulsos de uma empresa (o `{alvo}` aqui é o CNPJ) description: | Roda **sem limite** — sujeito ao teto padrão do banco. NÃO CONFIRMADO: envelope da resposta. O único campo confirmado é `valor` (reais). security: - JwtOperador: [] - ChaveInterna: [] parameters: - name: alvo in: path required: true description: CNPJ com 14 dígitos, sem máscara. schema: { type: string, pattern: '^\d{14}$' } example: '12345678000190' - name: competencia in: query description: Igualdade exata. schema: { $ref: '#/components/schemas/Competencia' } responses: '200': description: 'NÃO CONFIRMADO: envelope da resposta.' content: application/json: schema: { type: object, additionalProperties: true } put: tags: [Faturamento e apuração] operationId: alterarAvulso summary: Altera um lançamento avulso (o `{alvo}` aqui é o id do lançamento) description: 'NÃO CONFIRMADO: corpo aceito e shape da resposta.' security: - JwtOperador: [] - ChaveInterna: [] parameters: - name: alvo in: path required: true description: Id do lançamento avulso. schema: { type: string } requestBody: required: true content: application/json: schema: { type: object, additionalProperties: true } responses: '200': description: 'NÃO CONFIRMADO: shape da resposta.' content: application/json: schema: { type: object, additionalProperties: true } delete: tags: [Faturamento e apuração] operationId: apagarAvulso summary: Apaga um lançamento avulso (o `{alvo}` aqui é o id do lançamento) description: | Também apaga a linha espelho na contabilidade. NÃO CONFIRMADO: shape da resposta. security: - JwtOperador: [] - ChaveInterna: [] parameters: - name: alvo in: path required: true description: Id do lançamento avulso. schema: { type: string } responses: '200': description: 'NÃO CONFIRMADO: shape da resposta.' content: application/json: schema: { type: object, additionalProperties: true } /api/avulsos/criar: post: tags: [Faturamento e apuração] operationId: criarAvulso summary: Cria um lançamento avulso description: | Obrigatórios: `cnpj`, `competencia`, `valor` (> 0) e `motivo`. A autoria fica gravada em `extra_data.lancado_por`. NÃO CONFIRMADO: shape da resposta. security: - JwtOperador: [] - ChaveInterna: [] requestBody: required: true content: application/json: schema: type: object properties: cnpj: { type: string, pattern: '^\d{14}$' } competencia: { $ref: '#/components/schemas/Competencia' } valor: { type: number, exclusiveMinimum: 0 } motivo: { type: string } tipoReceita: { type: string } anexo: { $ref: '#/components/schemas/AnexoSimples' } tomador: { type: string } observacao: { type: string } clienteAvisado: { type: boolean } lancadoPor: { type: string } required: [cnpj, competencia, valor, motivo] additionalProperties: false responses: '200': description: 'NÃO CONFIRMADO: shape da resposta.' content: application/json: schema: { type: object, additionalProperties: true } ############################################################################ # API DA EMPRESA ############################################################################ /api/cliente/v1: get: tags: [API da empresa (chave por empresa)] operationId: indiceApiEmpresa summary: Índice auto-documentado da API da empresa description: | Responde **HTML** para navegador e **JSON** para programa (depende do `Accept`). NÃO CONFIRMADO: shape do JSON. security: - ChaveEmpresaBearer: [] - ChaveEmpresaHeader: [] - {} responses: '200': description: Índice. content: application/json: schema: { type: object, additionalProperties: true } text/html: schema: { type: string } /api/cliente/v1/{recurso}: get: tags: [API da empresa (chave por empresa)] operationId: lerRecursoEmpresa summary: Lê um recurso da própria empresa description: | **A empresa sai do token.** Nenhum recurso aceita CNPJ como parâmetro, e qualquer query param fora da lista aceita é **descartado em silêncio** (inclusive `cnpj`). Valores são truncados em 40 caracteres. Query params aceitos, por recurso — e **só** estes: - `faturamento`: `ano` (4 dígitos) - `impostos`: `ano` (4 dígitos) - `documentos`: `competencia` (`AAAA-MM`) - todos os demais: **nenhum** Ações de leitura (ver `POST /api/cliente/v1/{acao}`) também respondem por GET nesta rota. Ação de **escrita** chamada por GET **não** dá 405: dá **404 "recurso não existe"**. Escopo exigido: `leitura`. ⚠️ Falha de leitura no serviço de origem sai como **502**, não como 4xx. x-escopo: leitura security: - ChaveEmpresaBearer: [] - ChaveEmpresaHeader: [] parameters: - name: recurso in: path required: true schema: type: string enum: - empresa - resumo - faturamento - impostos - notas - apuracao - folha - extrato - documentos - parcelamento - contrato - situacao_fiscal - caixa_postal - notificacoes - drive - modulos - processos - contas_bancarias - indicacoes - name: ano in: query description: Só para `faturamento` e `impostos`. schema: { type: string, pattern: '^\d{4}$' } - name: competencia in: query description: Só para `documentos`. schema: { $ref: '#/components/schemas/Competencia' } responses: '200': description: | Conteúdo do recurso. O shape varia por recurso e espelha a rota `portal-api` correspondente. content: application/json: schema: { type: object, additionalProperties: true } '401': { $ref: '#/components/responses/NaoAutenticado' } '404': description: | Recurso inexistente — **ou uma ação de escrita chamada por GET**. A resposta traz a lista de recursos válidos, o que ajuda no diagnóstico. content: application/json: schema: type: object properties: ok: { const: false } error: { const: recurso não existe } recursos: { type: array, items: { type: string } } '502': description: A leitura falhou no serviço de origem. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: ok: false error: não consegui ler faturamento agora (HTTP 500) /api/cliente/v1/{acao}: post: tags: [API da empresa (chave por empresa)] operationId: executarAcaoEmpresa summary: Executa uma ação sobre a própria empresa description: | Roteamento por **lista de nomes**, não por verbo. As 8 ações marcadas abaixo exigem chave de escopo `completo`; as demais funcionam com `leitura`. | Ação | Escopo | |---|---| | `contas_a_pagar` | leitura | | `nota_pdf` | leitura | | `guia_pdf` | leitura | | `documento_pdf` | leitura | | `emitir_nota` | **completo** | | `lancar_faturamento` | **completo** | | `atualizar_notas` | **completo** | | `bancos` | leitura | | `banco_lancamentos` | leitura | | `banco_a_receber` | leitura | | `banco_sincronizar` | **completo** | | `openfinance_bancos` | leitura | | `openfinance_conectar` | **completo** | | `meus_clientes` | leitura | | `minhas_cobrancas` | leitura | | `gerar_cobranca` | **completo** | | `gerar_guia` | **completo** | | `cancelar_nota` | **completo** | ⚠️ **Nomes traiçoeiros:** `meus_clientes` e `minhas_cobrancas` **não** são a carteira do escritório nem as notas fiscais da empresa. São os clientes e as cobranças **que a própria empresa emite**, lidos de outro banco (o do painel do cliente). Se a empresa não tiver conta lá, a ação falha com "esta empresa ainda não tem conta no painel do cliente". `minhas_cobrancas` aceita `situacao` = `em aberto | pagas | todas`, com teto fixo de 120 linhas e sem paginação. ⚠️ Ações que geram guia podem **custar dinheiro** (consulta paga a órgão externo). Não chame `gerar_guia` em teste. security: - ChaveEmpresaBearer: [] - ChaveEmpresaHeader: [] parameters: - name: acao in: path required: true schema: type: string enum: - contas_a_pagar - nota_pdf - guia_pdf - documento_pdf - emitir_nota - lancar_faturamento - atualizar_notas - bancos - banco_lancamentos - banco_a_receber - banco_sincronizar - openfinance_bancos - openfinance_conectar - meus_clientes - minhas_cobrancas - gerar_cobranca - gerar_guia - cancelar_nota requestBody: required: false description: | Argumentos da ação. NÃO CONFIRMADO: schema por ação — consulte `tools/list` na fachada MCP da empresa. content: application/json: schema: { type: object, additionalProperties: true } responses: '200': description: 'NÃO CONFIRMADO: envelope exato do sucesso.' content: application/json: schema: { type: object, additionalProperties: true } '400': description: | A ação executou e falhou (regra de negócio). Mensagem truncada em 300 caracteres — é aqui que chegam "mês fechado", "data fora da competência", duplicidade de nota. content: application/json: schema: { $ref: '#/components/schemas/Erro' } '401': { $ref: '#/components/responses/NaoAutenticado' } '403': description: Ação de escrita com chave de leitura. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: ok: false error: esta chave é somente leitura — gere uma chave com permissão de escrita no painel '404': description: Ação inexistente. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: { ok: false, error: ação não existe } /api/cliente/v1/download/{id}: get: tags: [API da empresa (chave por empresa)] operationId: baixarArquivoEmpresa summary: Baixa um arquivo gerado por uma ação description: | Link de **uso único**, válido por 10 minutos. security: - ChaveEmpresaBearer: [] - ChaveEmpresaHeader: [] - {} parameters: - name: id in: path required: true schema: { type: string } responses: '200': description: O arquivo. content: application/octet-stream: schema: { type: string, format: binary } '410': description: Link vencido ou já usado. **Corpo em HTML**, não JSON. content: text/html: schema: { type: string } '424': description: | Não foi possível buscar o arquivo na origem. **Corpo em texto puro.** É 424 e não 5xx de propósito — o proxy do ambiente sequestra 5xx. content: text/plain: schema: { type: string } example: não consegui buscar o arquivo agora ############################################################################ # CHAVES DA EMPRESA (portal-api) ############################################################################ /api/portal-api/{cnpj}/chaves: get: tags: [portal-api (server↔server), Chaves e auditoria] operationId: listarChavesEmpresa summary: Lista as chaves de API de uma empresa description: O hash da chave nunca é devolvido. security: - PortalApiKeyHeader: [] - PortalApiKeyBearer: [] parameters: - $ref: '#/components/parameters/Cnpj' responses: '200': description: Chaves e últimos acessos. content: application/json: schema: type: object properties: ok: { const: true } chaves: type: array items: type: object properties: id: { type: string, format: uuid } prefixo: { type: string, description: Os 12 primeiros caracteres da chave. } rotulo: { type: [string, 'null'] } ativo: { type: boolean } escopo: { type: string, enum: [leitura, completo] } criado_em: { type: string, format: date-time } ultimo_uso_em: { type: [string, 'null'], format: date-time } usos: { type: integer } ultimos_acessos: type: array maxItems: 20 items: { type: object, additionalProperties: true } required: [ok, chaves] '401': { $ref: '#/components/responses/PortalNaoAutenticado' } '404': description: Empresa não encontrada. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: { ok: false, error: cliente não encontrado } post: tags: [portal-api (server↔server), Chaves e auditoria] operationId: criarChaveEmpresa summary: Emite uma chave de empresa (`ctb_`) description: | Teto de **5 chaves ativas por empresa** (409 quando estoura). O valor cru sai uma única vez. Esta rota **não grava validade** — a chave não expira. Qualquer `escopo` diferente da string exata `completo` vira `leitura`. O `rotulo` é truncado em 80 caracteres. security: - PortalApiKeyHeader: [] - PortalApiKeyBearer: [] parameters: - $ref: '#/components/parameters/Cnpj' requestBody: required: false content: application/json: schema: type: object properties: rotulo: { type: string, maxLength: 80 } escopo: { type: string, enum: [leitura, completo], default: leitura } criado_por: { type: string } additionalProperties: false responses: '200': description: Chave criada. content: application/json: schema: type: object properties: ok: { const: true } token: { type: string, description: '`ctb_` + 32 chars base64url = 36 caracteres.' } id: { type: string, format: uuid } prefixo: { type: string } escopo: { type: string, enum: [leitura, completo] } aviso: { type: string } required: [ok, token, id, prefixo, escopo] example: ok: true token: ctb_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX id: 2a7c0d9e-0d1a-4d0e-9b1c-5f2b7f0a1c33 prefixo: ctb_XXXXXXXX escopo: leitura aviso: 'guarde agora: esta chave não será mostrada de novo' '401': { $ref: '#/components/responses/PortalNaoAutenticado' } '404': description: Empresa não encontrada. content: application/json: schema: { $ref: '#/components/schemas/Erro' } '409': description: Teto de 5 chaves ativas. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: ok: false error: você já tem 5 chaves ativas — revogue uma antes de criar outra /api/portal-api/{cnpj}/chaves/{id}/revogar: post: tags: [portal-api (server↔server), Chaves e auditoria] operationId: revogarChaveEmpresa summary: Revoga uma chave de empresa description: | Sem corpo. A revogação vale na hora. O filtro por empresa faz parte da condição do UPDATE — não dá para revogar chave de outra empresa sabendo o id. security: - PortalApiKeyHeader: [] - PortalApiKeyBearer: [] parameters: - $ref: '#/components/parameters/Cnpj' - name: id in: path required: true schema: { type: string, format: uuid } responses: '200': description: Revogada. content: application/json: schema: type: object properties: ok: { const: true } revogada: { type: string, description: O prefixo da chave revogada. } required: [ok, revogada] '401': { $ref: '#/components/responses/PortalNaoAutenticado' } '404': description: Chave (ou empresa) não encontrada. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: { ok: false, error: chave não encontrada } ############################################################################ # DIAGNÓSTICO ############################################################################ /api/health: get: tags: [Diagnóstico] operationId: health summary: Saúde do backend description: | Rota pública, sem autenticação. NÃO CONFIRMADO: shape do corpo (só a abertura da rota foi confirmada). security: [{}] responses: '200': description: OK. content: application/json: schema: { type: object, additionalProperties: true } /api/version: get: tags: [Diagnóstico] operationId: version summary: Commit em execução no backend description: | Rota pública, sem autenticação. NÃO CONFIRMADO: shape do corpo. security: [{}] responses: '200': description: OK. content: application/json: schema: { type: object, additionalProperties: true } components: securitySchemes: ChaveOperadorBearer: type: http scheme: bearer description: | **Chave de operador** (tabela `mcp_tokens`, prefixo `cnt_`, 68 caracteres). É a credencial recomendada para um sistema externo. `Authorization: Bearer SUA_CHAVE` ⚠️ O parser é **sensível a maiúsculas e exige exatamente um espaço**: `bearer x` ou `Bearer x` (dois espaços) não são reconhecidos e caem no fallback do header `X-Mcp-Token`, resultando em 401. O escopo (`leitura` | `completo`) e os módulos ficam gravados na chave, não no header. Chave revogada ou expirada devolve o mesmo 401 de chave inexistente. ChaveOperadorHeader: type: apiKey in: header name: X-Mcp-Token description: | Alternativa ao Bearer para a mesma chave de operador. Valor cru, sem prefixo de esquema. **Não existe chave no path nem em query string nesta superfície.** ChaveEmpresaBearer: type: http scheme: bearer description: | **Chave da empresa** (tabela `cliente_api_tokens`, prefixo `ctb_`, 36 caracteres). Dá acesso somente aos dados daquela empresa — a empresa sai do token. ChaveEmpresaHeader: type: apiKey in: header name: X-Api-Key description: | Mesma chave de empresa, por header. ⚠️ **Não confundir** com o `X-Api-Key` da `portal-api`, que é uma chave global de servidor e uma coisa completamente diferente. PortalApiKeyHeader: type: apiKey in: header name: X-Api-Key description: | **Chave global de processo** da camada `portal-api` (server↔server). Não pertence a ninguém em particular: não tem empresa, não tem escopo, não tem auditoria por consumidor. Quem a tem alcança **qualquer CNPJ** passando o CNPJ no path. Comparação em tempo constante; chave com menos de 32 caracteres bloqueia tudo. 🚫 **Não entregar a integrador de terceiro.** É a mesma chave que o painel do cliente usa. PortalApiKeyBearer: type: http scheme: bearer description: A mesma chave global de `portal-api`, aceita também como Bearer. JwtOperador: type: http scheme: bearer bearerFormat: JWT description: | JWT de sessão do operador (Supabase Auth). Nasce de um **login humano** e expira — não serve como credencial de máquina para um sistema externo. O token também é aceito por query string (`?access_token=`) em rotas de mídia, e fica em cache de memória por 30 minutos — ou seja, **revogar sessão não vale na hora**. ⚠️ Validar o JWT prova apenas que ele é válido: não há checagem de cadastro de operador, de administrador, nem de permissão de módulo nessa verificação. ChaveInterna: type: apiKey in: header name: x-internal-key description: | 🚫 **Passe livre. Nunca entregar a terceiros.** Aceita antes de qualquer checagem de JWT e libera **todas** as rotas `/api/*`, escrita inclusive. Pior: com ela, o escritório da requisição passa a ser decidido pelo header `X-Operator-Email` cru — quem tem a chave escolhe o escritório digitando um e-mail. Documentada aqui só para que o integrador **reconheça e recuse** essa credencial caso alguém a ofereça. parameters: Cnpj: name: cnpj in: path required: true description: CNPJ com 14 dígitos, **sem máscara**. schema: type: string pattern: '^\d{14}$' example: '12345678000190' ClienteId: name: id in: path required: true description: | UUID da empresa. A maioria das rotas aceita também o `legacy_id` (id textual do sistema antigo) como alternativa — mas **não todas**: `POST /api/clients/{id}/cancel` só aceita UUID. schema: { type: string } example: 6f1f0f5a-1c3e-4a2b-9f0d-6b6a2f0f9c11 responses: FerramentaOk: description: Ferramenta executada. content: application/json: schema: { $ref: '#/components/schemas/RespostaFerramenta' } example: ok: true resultado: {} NaoAutenticado: description: | Chave ausente, inválida, revogada (`ativo = false`) ou expirada — todos os casos dão a mesma resposta, sem distinção. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: ok: false error: chave inválida ou revogada NaoAutenticadoOperador: description: A rota exige JWT de operador (ou a chave interna). content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: ok: false error: requer operador autenticado PortalNaoAutenticado: description: Chave da camada `portal-api` ausente ou errada. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: ok: false error: unauthorized SemPermissao: description: | Duas causas possíveis, distinguíveis pela mensagem: a chave não tem o **módulo** da ferramenta, ou a chave é de **leitura** e a ferramenta escreve. content: application/json: schema: { $ref: '#/components/schemas/Erro' } examples: modulo: summary: Módulo fora da chave value: ok: false error: sua chave não tem acesso ao módulo "fiscal" somenteLeitura: summary: Chave de leitura tentando escrever value: ok: false error: esta chave é somente leitura — posso consultar e montar o passo a passo, mas quem aplica é você na tela ErroDeExecucao: description: | Autenticou, passou nas travas de módulo e escopo, e a ferramenta falhou. A mensagem é a do erro original, **truncada em 400 caracteres**. Cai aqui: argumento obrigatório faltando, empresa não encontrada, tabela não liberada, erro do banco. content: application/json: schema: { $ref: '#/components/schemas/Erro' } example: ok: false error: 'não achei o cliente "Maximus"' MuitasChamadas: description: | Teto de **120 chamadas por minuto por chave**, em janela deslizante. Não há header `Retry-After` nessa camada — o tempo de espera vem no corpo, em `tentar_em_segundos`. Existe ainda um segundo freio, **por IP**, de 1500/min, que devolve outro corpo (`rate-limit: muitas requisições, tente em 1min`) e aí sim manda os headers padrão. headers: RateLimit-Policy: description: 'Só no freio por IP. Ex.: `1500;w=60`.' schema: { type: string } RateLimit: description: 'Só no freio por IP. Ex.: `limit=1500, remaining=0, reset=42`.' schema: { type: string } Retry-After: description: Só no freio por IP, e só quando estoura. Em segundos. schema: { type: integer } content: application/json: schema: type: object properties: ok: { const: false } error: { type: string } tentar_em_segundos: type: integer description: Presente só no freio por chave. required: [ok, error] examples: porChave: value: ok: false error: muitas chamadas seguidas — espere 37s tentar_em_segundos: 37 porIp: value: ok: false error: 'rate-limit: muitas requisições, tente em 1min' ErroInterno: description: Erro não tratado. content: application/json: schema: { $ref: '#/components/schemas/Erro' } schemas: Erro: type: object title: Erro description: Envelope de erro padrão da maioria das rotas. properties: ok: { const: false } error: { type: string } required: [ok, error] RespostaFerramenta: type: object title: RespostaFerramenta description: | Envelope de sucesso de `POST /api/v1/ferramentas/{nome}`. O `resultado` é a saída crua da ferramenta, **sem truncamento** (a fachada MCP corta em 120.000 caracteres; a REST não). properties: ok: { const: true } resultado: {} required: [ok, resultado] FerramentaDoCatalogo: type: object title: FerramentaDoCatalogo properties: nome: { type: string } modulo: type: string enum: [contabil, fiscal, folha, empresas, incubadora, financeiro] escreve: type: boolean description: Se `true`, exige chave de escopo `completo`. descricao: { type: string } parametros: type: object description: JSON Schema dos argumentos aceitos. **É a fonte da verdade do corpo.** additionalProperties: true required: [nome, modulo, escreve, descricao, parametros] Modulo: type: object title: Modulo description: | Um dos 6 módulos de escopo. `permissao` e `permissaoAlterar` são as chaves de permissão do operador correspondentes — repare que os nomes **não** batem com a chave do módulo (`empresas` → `clients`, `financeiro` → `financial`). properties: key: type: string enum: [contabil, fiscal, folha, empresas, incubadora, financeiro] label: { type: string } sub: { type: string } permissao: { type: string } permissaoAlterar: { type: string } required: [key, label, sub, permissao, permissaoAlterar] Competencia: type: string title: Competencia description: | Mês de referência no formato `AAAA-MM`. **Não é data ISO** — não mande `AAAA-MM-DD` em campo de competência. pattern: '^\d{4}-\d{2}$' example: '2026-06' StatusCliente: type: string title: StatusCliente description: | Os 6 valores aceitos pela rota de troca de status. A interface hoje só oferece 5 (`archived` foi removido da tela em 16/08/2026, mas continua válido na API). **Só `active` é considerado operacional.** enum: [lead, fechando_contrato, active, paused, cancelled, archived] AnexoSimples: type: string title: AnexoSimples description: | Anexo do Simples Nacional. I = Comércio; II = Indústria; III = Serviços em geral; IV = Serviços (advocacia, construção); V = Serviços sujeitos ao Fator R. enum: [I, II, III, IV, V] EmpresaListada: type: object title: EmpresaListada description: Item de `empresas_listar`. Estes são exatamente os campos selecionados. properties: name: { type: string } cnpj: { type: [string, 'null'], description: 14 dígitos, sem máscara. } regime: { type: [string, 'null'] } anexo: { type: [string, 'null'] } status: { type: [string, 'null'] } municipio: { type: [string, 'null'] } uf: { type: [string, 'null'] } monthly_value: type: [number, 'null'] description: | Honorário do **cadastro**, em reais. Fonte secundária e quase sempre vazia — o valor contratado está no contrato assinado. EmpresaBusca: type: object title: EmpresaBusca description: Item de `clientes_buscar`. properties: id: { type: string } name: { type: string } cnpj: { type: [string, 'null'] } regime: { type: [string, 'null'] } status: { type: [string, 'null'] } EmpresaFicha: type: object title: EmpresaFicha description: | Saída de `empresa_ficha`. São cerca de 30 campos; os listados abaixo foram confirmados no código, e `additionalProperties` fica aberto para os demais. properties: inscricao_estadual: type: [string, 'null'] description: | ⚠️ Divergência conhecida: esta ferramenta lê `inscricao_estadual` como coluna, enquanto a tela do painel grava a inscrição em outro lugar (`extra_data.sintegra_ie`). Pode vir vazio mesmo com o painel mostrando valor. inscricao_municipal: { type: [string, 'null'] } monthly_value: { type: [number, 'null'] } due_day: type: [integer, 'null'] description: Dia do vencimento do honorário. plan_start_date: type: [string, 'null'] description: 'Data de início do plano (`AAAA-MM-DD`). É o campo mais próximo de "cliente desde".' additionalProperties: true HonorarioEmpresa: type: object title: HonorarioEmpresa description: Item de `financeiro_honorarios`. Campos exatamente como selecionados. properties: name: { type: string } cnpj: { type: [string, 'null'] } regime: { type: [string, 'null'] } status: { type: [string, 'null'] } monthly_value: { type: [number, 'null'], description: Reais. } due_day: { type: [integer, 'null'] } is_permuta: { type: [boolean, 'null'] } permuta_valor: { type: [number, 'null'] } permuta_contrapartida: { type: [string, 'null'] } asaas_id: type: [string, 'null'] description: Vazio significa empresa sem cobrança montada. plan_start_date: { type: [string, 'null'] } ContratoTabela: type: object title: ContratoTabela description: Item de `financeiro_contratos` (tabela `contracts`). properties: id: { type: string } client_id: { type: [string, 'null'] } status: { type: [string, 'null'] } monthly_value: { type: [number, 'null'], description: Reais. } signed_by: { type: [string, 'null'] } signed_at: { type: [string, 'null'], format: date-time } created_at: { type: [string, 'null'], format: date-time } ContratoResumo: type: object title: ContratoResumo description: | Resumo do contrato assinado devolvido pela `portal-api`. Os três campos abaixo foram confirmados; pode haver outros. properties: valor_mensal: type: [number, 'null'] description: | Valor mensal em reais, **já líquido de desconto**. Esta é a fonte da verdade do honorário contratado. vigencia: type: [string, 'null'] description: | ⚠️ **Não é data de início de vigência do valor.** Para contrato de contabilidade mensal vem a string literal `Indeterminado`; para serviço extraordinário, uma data `AAAA-MM-DD` (assinatura + 30 dias). assinado_em: type: [string, 'null'] format: date-time description: O proxy mais próximo de "desde quando esse valor vale". additionalProperties: true ClienteCurado: type: object title: ClienteCurado description: | Item de `GET /api/clients`. **Não é a linha crua** — é um objeto montado à mão. Estes são exatamente os campos devolvidos. Repare no que **não** está aqui: `monthly_value`, endereço estruturado, `contacts`. properties: id: type: string description: | ⚠️ Pode ser o UUID **ou** o `legacy_id`, dependendo da empresa (a camada de compatibilidade prefere o `legacy_id` quando ele existe). cnpj: { type: string, description: Sempre 14 dígitos — linhas com CNPJ diferente disso são descartadas. } name: { type: [string, 'null'] } razao_social: { type: [string, 'null'] } regime: { type: [string, 'null'] } status: { type: [string, 'null'] } phone: { type: [string, 'null'], description: 'Formatado, ex.: `(47) 99999-9999`.' } email: { type: [string, 'null'] } uf: { type: [string, 'null'] } municipio: { type: [string, 'null'] } cnae_fiscal: { type: [string, 'null'] } cnae_fiscal_descricao: { type: [string, 'null'] } cnaes_secundarios: { type: [array, 'null'], items: {} } certificado_validade: { type: [string, 'null'] } certificado_outorgado: {} procuracao_outorgada: {} procuracao_validade: { type: [string, 'null'] } sem_contabilidade: {} tipo_portal: { type: [string, 'null'] } sigiss_cidade: { type: [string, 'null'] } sigiss_senha_ws_configured: { type: [boolean, 'null'] } prefeitura_password_configured: { type: [boolean, 'null'] } fiscal_password_configured: { type: [boolean, 'null'] } portal_login_via_cert: {} data_inicio_atividade: { type: [string, 'null'] } category_ids: { type: [array, 'null'], items: {} } incubadora_hidden: {} indicador_id: { type: [string, 'null'] } indicador_nome: { type: [string, 'null'] } indicacao_modo: { type: [string, 'null'] } additionalProperties: true LinhaImportCsv: type: object title: LinhaImportCsv properties: linha: { type: integer, description: Número da linha no arquivo (1 = cabeçalho). } cnpj: { type: string, description: Só dígitos. } nome: { type: string } status: type: string enum: [criado, duplicado, invalido, dryrun_ok, erro] error: type: string description: Presente só em `invalido` e `erro`. required: [linha, cnpj, status] AvulsoResumo: type: object title: AvulsoResumo description: Lançamento avulso, como aparece dentro do faturamento mensal. properties: id: { type: string } valor: { type: number, description: Reais. } descricao: { type: [string, 'null'] } por_cliente: { type: [boolean, 'null'] } RpcRequisicao: type: object title: RpcRequisicao description: Envelope JSON-RPC 2.0. properties: jsonrpc: { const: '2.0' } id: { type: [string, number, 'null'] } method: type: string description: 'Ex.: `initialize`, `ping`, `tools/list`, `tools/call`, `notifications/*`.' params: { type: object, additionalProperties: true } required: [jsonrpc, method] RpcResposta: type: object title: RpcResposta description: | Sucesso JSON-RPC. Em `tools/call`, o **erro de negócio** também vem aqui, com `result.isError = true` e HTTP 200. properties: jsonrpc: { const: '2.0' } id: { type: [string, number, 'null'] } result: type: object properties: content: type: array items: type: object properties: type: { type: string } text: { type: string } additionalProperties: true isError: { type: boolean } additionalProperties: true error: { $ref: '#/components/schemas/RpcErroObjeto' } required: [jsonrpc] RpcErro: type: object title: RpcErro properties: jsonrpc: { const: '2.0' } id: { type: [string, number, 'null'] } error: { $ref: '#/components/schemas/RpcErroObjeto' } required: [jsonrpc, error] RpcErroObjeto: type: object title: RpcErroObjeto description: | Códigos usados: `-32001` chave inválida/revogada (HTTP 401); `-32002` teto de chamadas (HTTP 429); `-32601` método não suportado (HTTP 200); `-32603` exceção não tratada (HTTP 500, mensagem truncada em 300 caracteres). properties: code: { type: integer } message: { type: string } required: [code, message]