openapi: 3.0.3
info:
  title: Econodata API v4
  version: "4.0"
  description: |
    A API v4 é a via programática da Econodata, a solução de Go-To-Market de Precisão:
    a inteligência que segmenta o mercado brasileiro pelo tamanho e perfil real das empresas,
    para alimentar CRM, automações e agentes (MCP) em tempo real.

    API pública da Econodata (v4). Autenticação por API key (`Authorization: Bearer ek_live_...`).
    Quem já é cliente gera a chave na plataforma, em [Chaves e Integrações](https://plat.econodata.com.br/#/integracoes-api); a tela só aparece em contas com a API v4 liberada, e a liberação é feita pelo time da Econodata.
    Para avaliar a API, a chave trial vem do [cadastro do trial](https://www.econodata.com.br/api-trial?utm_source=docs).
    Cobrança em **tokens**: cada chamada debita a base do endpoint mais os campos entregues;
    re-acesso à mesma empresa dentro de 24h não recobra. O saldo é
    consultado em `GET /v4/account/balance`. A tabela de valores por campo/item está em
    definição e não é publicada aqui — use `X-Estimate-Only` para o custo exato de cada chamada.

    Base de produção: `https://api.econodata.com.br`. Todos os exemplos deste documento
    refletem produção.

    **Headers de resposta**
    - `X-Tokens-Charged`: tokens debitados na chamada (0 quando grátis, deduplicado ou 4xx).
    - `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset`: limite por chave.

    **💡 Simule o custo antes de gastar (`X-Estimate-Only`)**
    Todo endpoint de dados aceita o header de requisição **`X-Estimate-Only: true`** (dry-run):
    a API calcula quantos tokens a chamada consumiria e devolve `{ tokensEstimados }` **sem
    debitar e sem entregar dado** (`X-Tokens-Charged: 0`). Use antes de qualquer chamada paga
    — em vez de dar tiro no escuro, você orça o custo exato (buckets/`incluir`, itens por lista,
    lote) e só então confirma. Disponível em: `companies` (lote), `companies/search`,
    `companies/match`, `companies/search_list`(+`_by_id`), `companies/calc_list`(+`_by_id`),
    `companies/{cnpj}/people` e `companies/economic-group`.

    **Acesso trial (chave `is_trial`) — read-only**
    Chaves de trial são **somente leitura**: liberam consulta e segmentação, mas **não**
    escrita nem desbloqueio. Permitido: lookup (`companies`, `companies/search`), `match`,
    `search_list` por filtros, `people`, `economic-group` e `account/balance`. **Bloqueado
    (403):** `desbloquear:true` (desbloqueio consome crédito real de plataforma), tags
    (`POST`/`DELETE /companies/tags`, `GET /companies/tags/jobs/{id}`) e segmentação por
    **pesquisa salva** (`saved_searches`, `search_list_by_id`, `calc_list_by_id`). A geração e
    administração de chaves **não** faz parte do trial: a chave de trial é provisionada pelo
    [cadastro do trial da API](https://www.econodata.com.br/api-trial?utm_source=docs), no site da Econodata (tela separada), não pela tela de
    autoatendimento de chaves.

    **Limites**: 60 requisições por minuto por chave (rajada até 120). Excedente → **429**.
    Saldo de tokens insuficiente → **402**.
  contact:
    name: Econodata
    url: https://www.econodata.com.br
servers:
  - url: https://api.econodata.com.br
    description: Produção

security:
  - bearerAuth: []

tags:
  - name: companies
    description: Operações sobre empresas
  - name: account
    description: Conta e saldo de tokens do cliente

paths:
  /v4/companies:
    post:
      tags: [companies]
      operationId: lookupCompaniesByCnpjs
      x-required-scope: companies:read
      summary: Lookup de empresas em lote por CNPJ
      description: |
        Recebe de 1 a 100 CNPJ (com ou sem máscara) e devolve as empresas
        encontradas + um erro por CNPJ não encontrado. Read-only. Responde 404
        somente quando nenhuma empresa é encontrada.
      parameters:
        - $ref: '#/components/parameters/XEstimateOnly'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CompaniesBatchRequest'
            examples:
              porCnpjs:
                summary: Dois CNPJs, expandindo perfil
                value:
                  cnpjs: ['47960950000121', '11.111.111/1111-11']
                  incluir: [cadastro]
      responses:
        '200':
          description: Empresas encontradas (+ erros por CNPJ não encontrado)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompaniesBatchResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Nenhuma das empresas informadas foi encontrada
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          description: Fonte de dados de empresa indisponível
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'

  /v4/companies/match:
    post:
      tags: [companies]
      operationId: matchCompany
      x-required-scope: companies:match
      summary: Match inteligente de empresa por critérios soltos
      description: |
        Recebe um conjunto de critérios (nome, UF, site, telefone, e-mail, CNPJ)
        e devolve o melhor match com score de confiança, indicando em quais campos
        o match foi feito. Pelo menos um critério deve ser informado.
      parameters:
        - $ref: '#/components/parameters/XEstimateOnly'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MatchRequest'
            examples:
              porSite:
                summary: Match por site + nome
                value:
                  criterios:
                    nome: Magazine Luiza
                    uf: SP
                    site: magazineluiza.com.br
                  incluir: [cadastro]
      responses:
        '200':
          description: |
            Top-N (até 5) melhores matches, ordenados (melhor primeiro). Lista vazia
            = nada encontrado (grátis, sem 404). O melhor vem com `empresa` quando
            confiante (único cobrado); os demais são só identificação.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MatchResponse'
              examples:
                ok:
                  summary: Melhor match confiante + alternativas
                  value:
                    correspondencias:
                      - cnpj: '47.960.950/0001-21'
                        confianca: 0.92
                        correspondidoEm: [nome]
                        razaoSocial: MAGAZINE LUIZA S/A
                        nomeFantasia: Magazine Luiza
                        empresa:
                          cnpj: '47.960.950/0001-21'
                      - cnpj: '33.905.522/0001-91'
                        confianca: 0.81
                        correspondidoEm: [nome]
                        razaoSocial: MAGAZINE LUIZA
                        nomeFantasia: Magazine Luiza Filial
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          description: Nenhum match encontrado para os critérios informados
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          description: Fonte de dados de empresa indisponível
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'

  /v4/companies/search:
    get:
      tags: [companies]
      operationId: lookupCompanyByQuery
      x-required-scope: companies:read
      summary: Lookup de empresa por identificador (query string)
      description: |
        Igual ao POST, com os identificadores na query string (consulta
        idempotente). Pelo menos um identificador deve ser informado.
        Aceita o header `X-Estimate-Only` (preview de custo, sem cobrar).
      parameters:
        - $ref: '#/components/parameters/XEstimateOnly'
        - in: query
          name: cnpj
          schema: { type: string }
          description: CNPJ com ou sem máscara — normalizado no servidor.
          example: '47960950000121'
        - in: query
          name: site
          schema: { type: string }
          description: Site/domínio (com ou sem protocolo/www).
          example: econodata.com.br
        - in: query
          name: email
          schema: { type: string, format: email }
          example: contato@econodata.com.br
        - in: query
          name: raizCnpj
          schema: { type: string }
          description: Raiz do CNPJ (8 primeiros dígitos), com ou sem máscara.
          example: '47960950'
        - in: query
          name: incluir
          description: 'Expansões opcionais (CSV ou repetido): cadastro, estrategico, perfilNegocio, contatosBasicos, contatosAvancados.'
          required: false
          style: form
          explode: true
          schema:
            type: array
            items:
              type: string
              enum: [cadastro, estrategico, perfilNegocio, contatosBasicos, contatosAvancados]
          example: [cadastro, contatosBasicos]
        - in: query
          name: escopo
          description: 'Recorte de unidade: unidade (padrão), filiais ou matrizFiliais.'
          required: false
          schema:
            $ref: '#/components/schemas/Scope'
        - in: query
          name: campos
          description: 'Seleção de folhas por dot-path (CSV ou repetido), no formato `bucket.campo`. Auto-inclui o bucket da folha. Path que não existe no contrato → 422.'
          required: false
          style: form
          explode: true
          schema:
            type: array
            items:
              type: string
              minLength: 1
          example: [cadastro.razaoSocial, contatosBasicos.telefones]
        - in: query
          name: limite
          description: >-
            Cap de itens por lista entregue (ver schema Limite). Ausente → cap
            default (5); 0 = todos. O override por lista (limitePorLista) só está
            disponível no POST.
          required: false
          schema:
            $ref: '#/components/schemas/Limite'
      responses:
        '200':
          description: Empresa encontrada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyLookupResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Nenhuma empresa encontrada para os identificadores informados
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          description: Fonte de dados de empresa indisponível
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
    post:
      tags: [companies]
      operationId: lookupCompany
      x-required-scope: companies:read
      summary: Lookup de empresa por identificador
      description: |
        Recebe um identificador (CNPJ, site, e-mail ou raiz de CNPJ) e devolve a
        melhor empresa correspondente, enriquecida. Pelo menos um identificador
        deve ser informado. Read-only e idempotente.
        **Cobrança (tokens):** a consulta por CNPJ/raiz (chave direta) é mais barata que
        por site ou e-mail (resolução textual, mais cara). CNPJ tem precedência quando ambos
        são informados. Mais os campos entregues por bucket. (Valores por campo/item em definição.)
      parameters:
        - $ref: '#/components/parameters/XEstimateOnly'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CompanyLookupRequest'
            examples:
              porCnpj:
                summary: Lookup por CNPJ, expandindo perfil
                value:
                  criterios:
                    cnpj: '47.960.950/0001-21'
                  incluir: [cadastro]
      responses:
        '200':
          description: Empresa encontrada
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CompanyLookupResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Nenhuma empresa encontrada para os identificadores informados
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          description: Fonte de dados de empresa indisponível
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'

  /v4/companies/saved_searches:
    get:
      tags: [companies]
      operationId: listSavedSearches
      x-required-scope: companies:search_list_by_id
      summary: Lista as pesquisas salvas (dropdown de conector)
      description: |
        Lista as **pesquisas salvas compartilhadas** (`shared`) da conta — `nome`,
        `criadaEm` e `searchId` — para preencher o dropdown de um conector iPaaS.
        **Metadado da conta: não cobra tokens** (sem `X-Tokens-Charged`). Deduplicado
        por nome (mantém a mais recente, coerente com o desempate do `search_list_by_id`)
        e ordenado por criação desc.

        O valor a enviar em `search_list_by_id`/`calc_list_by_id` é o **`nome`**;
        `searchId` serve só para **desambiguar nomes repetidos** na UI (o casamento é
        por nome). Requer o escopo `companies:search_list_by_id` (chaves trial não têm → `403`).
      responses:
        '200':
          description: Pesquisas salvas compartilhadas da conta (pode ser vazia)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SavedSearchesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Credencial sem o escopo companies:search_list_by_id
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          description: Fonte de dados indisponível
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'

  /v4/companies/search_list:
    post:
      tags: [companies]
      operationId: searchCompaniesList
      x-required-scope: companies:search_list
      summary: Segmentação paginada por filtros
      description: |
        Segmenta empresas por **filtros diretos** (`filtros`, obrigatório). Devolve
        uma página de empresas. Paginação por cursor opaco; o cliente passa o
        `cursorProximo` recebido em `pagina.cursor` para avançar.

        **Cobrança (tokens):** a base do endpoint (pela segmentação) **mais** os campos
        entregues por empresa. Re-acessar a mesma empresa dentro de
        24h não recobra. Para obter só a contagem sem cobrar por campo, use `calc_list`.

        `incluir=contatosAvancados` não é suportado aqui — use `GET /v4/companies/{cnpj}/people`.
      parameters:
        - $ref: '#/components/parameters/XEstimateOnly'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SearchRequest'
            examples:
              primeiraPagina:
                summary: 1ª página — cobra a base + campos entregues
                value:
                  filtros:
                    situacao: [ATIVA]
                    uf: [SP, MG]
              paginaComEnriquecimento:
                summary: Página com incluir (base + campos por empresa)
                value:
                  filtros:
                    situacao: [ATIVA]
                    uf: [SP, MG]
                    porte: [MEDIO, GRANDE]
                    cnaePrimario: ['J-6204-0/00']
                    faturamento: { de: 1000000 }
                  pagina:
                    tamanho: 50
                  incluir: [cadastro]
              filtrosDoLote1:
                summary: Palavras-chave + presença de telefone + nível de atividade
                value:
                  filtros:
                    situacao: [ATIVA]
                    uf: [PR]
                    palavrasChave:
                      incluir: [padaria]
                      em: [NOME_EMPRESA]
                    comTelefones: true
                    nivelAtividade: [ALTA, MEDIA]
                  pagina:
                    tamanho: 50
      responses:
        '200':
          description: Página de resultados (sempre cobra base + campos entregues)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResponse'
              examples:
                ok:
                  summary: Página com cursor para próxima
                  value:
                    totalRegistros: 4827
                    pagina:
                      tamanho: 50
                      cursorProximo: Y3Vyc29yOjUw
                    resultados:
                      - cnpj: '47.960.950/0001-21'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          description: Fonte de dados de empresa indisponível
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'

  /v4/companies/search_list_by_id:
    post:
      tags: [companies]
      operationId: searchCompaniesListById
      x-required-scope: companies:search_list_by_id
      summary: Segmentação por pesquisa salva (nome)
      description: |
        Lista as empresas de uma **pesquisa salva** que o cliente criou na
        plataforma (`/#/prospeccao/pesquisas`), informando o **nome exato** (`nome`).
        A API resolve o nome → filtros salvos → executa a segmentação reusando o
        mesmo pipeline e billing do `search_list` (sempre cobra base + campos entregues).

        **Escopo (v1):** apenas pesquisas **compartilhadas** (`shared`) da conta. Nome
        não encontrado → `404`. Se a pesquisa usa um filtro que a API **ainda não
        suporta**, a chamada falha com `422` (sem cobrança) listando os filtros — a
        cobertura cresce a cada filtro portado. `incluir=contatosAvancados` não é suportado aqui.

        **Trial:** indisponível para chaves `is_trial` (requer `companies:search_list_by_id`) → **403**.
      parameters:
        - $ref: '#/components/parameters/XEstimateOnly'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SearchByIdRequest'
            examples:
              primeiraPagina:
                summary: 1ª página — cobra a base + campos entregues
                value:
                  nome: Bancos de São Paulo
              paginaComEnriquecimento:
                summary: Página com incluir (base + campos por empresa)
                value:
                  nome: Bancos de São Paulo
                  pagina:
                    tamanho: 50
                  incluir: [cadastro]
      responses:
        '200':
          description: Página de resultados (sempre cobra base + campos entregues)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          description: Nenhuma pesquisa salva compartilhada com esse nome na conta
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          description: Fonte de dados de empresa indisponível
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'

  /v4/companies/calc_list:
    post:
      tags: [companies]
      operationId: calcCompaniesList
      x-required-scope: companies:search_list
      summary: Contagem por filtros (quantas empresas a busca retorna)
      description: |
        Mesma segmentação por **filtros diretos** do `search_list`, mas devolve só a
        **contagem** total do conjunto (`{ totalRegistros }`) — sem empresas e sem
        paginação. **Gratuito** (sem billing): é o mesmo `totalRegistros` que a 1ª
        chamada do `search_list` já retorna.

        Aceita o mesmo corpo do `search_list`; `pagina`, `incluir` e `campos` são
        aceitos mas **ignorados** (não afetam a contagem).
      parameters:
        - $ref: '#/components/parameters/XEstimateOnly'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SearchRequest'
            examples:
              porFiltros:
                summary: Contagem de empresas ativas em SP/MG
                value:
                  filtros:
                    situacao: [ATIVA]
                    uf: [SP, MG]
      responses:
        '200':
          description: Contagem total do conjunto
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CalcResponse'
              examples:
                ok:
                  summary: Total de empresas que casam os filtros
                  value:
                    totalRegistros: 4827
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          description: Fonte de dados de empresa indisponível
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'

  /v4/companies/calc_list_by_id:
    post:
      tags: [companies]
      operationId: calcCompaniesListById
      x-required-scope: companies:search_list_by_id
      summary: Contagem por pesquisa salva (nome)
      description: |
        Mesma resolução de **pesquisa salva** (`nome`) do `search_list_by_id`, mas
        devolve só a **contagem** total (`{ totalRegistros }`) — sem empresas e sem
        paginação. **Gratuito** (sem billing).

        **Escopo (v1):** apenas pesquisas **compartilhadas** (`shared`) da conta. Nome
        não encontrado → `404`. Se a pesquisa usa um filtro que a API **ainda não
        suporta**, a chamada falha com `422`. `incluir`/`campos`/`pagina` são ignorados.

        **Trial:** indisponível para chaves `is_trial` (requer `companies:search_list_by_id`) → **403**.
      parameters:
        - $ref: '#/components/parameters/XEstimateOnly'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SearchByIdRequest'
            examples:
              porNome:
                summary: Contagem de uma pesquisa salva
                value:
                  nome: Bancos de São Paulo
      responses:
        '200':
          description: Contagem total do conjunto
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CalcResponse'
              examples:
                ok:
                  summary: Total de empresas da pesquisa salva
                  value:
                    totalRegistros: 312
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Nenhuma pesquisa salva compartilhada com esse nome na conta
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          description: Fonte de dados de empresa indisponível
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'

  /v4/companies/tags:
    post:
      tags: [companies]
      operationId: addCompanyTags
      x-required-scope: tags:write
      summary: Adiciona tags a empresas (migra POST /v3/update-lead)
      description: |
        Marca empresas (identificadas por cnpj, site ou e-mail) com uma ou mais
        tags. `marcarMatrizFiliais: true` propaga para todo o grupo Matriz-Filiais (mesma
        raiz de CNPJ). Escrita direta no índice de empresas + bookkeeping próprio
        (schema `api_v4`); não chama o serviço `ecdt-tags`. Gratuito (sem billing).
        Identificadores de e-mail/site marcam TODAS as empresas correspondentes.
        Devolve status individual por empresa.

        **Trial:** escrita de tags é indisponível para chaves `is_trial` (requer `tags:write`) → **403**.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApplyTagsRequest'
            examples:
              porCnpjEGrupo:
                summary: Marca uma empresa e o grupo Matriz-Filiais de outra
                value:
                  empresas:
                    - cnpj: '47.960.950/0001-21'
                    - site: empresa.com.br
                      marcarMatrizFiliais: true
                  tags: [cliente, priority]
      responses:
        '200':
          description: Status por empresa (sucesso/erro individual)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApplyTagsResponse'
              examples:
                ok:
                  summary: Uma marcada, uma não encontrada
                  value:
                    resultados:
                      - sucesso: true
                        cnpj: '47.960.950/0001-21'
                        mensagem: Tags adicionadas com sucesso.
                      - sucesso: false
                        cnpj: '00.000.000/0000-00'
                        mensagem: Empresa não encontrada.
        '202':
          description: |
            Lote grande (acima de 1000 empresas resolvidas, o teto síncrono) —
            enfileirado para processamento assíncrono. Consulte o progresso em
            `GET /v4/companies/tags/jobs/{id}`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TagJobAccepted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          description: Fonte de dados de tags indisponível
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
    delete:
      tags: [companies]
      operationId: removeCompanyTags
      x-required-scope: tags:write
      summary: Remove tags de empresas
      description: |
        Remove as tags informadas das empresas (mesma resolução do POST, incluindo
        grupo Matriz-Filiais). Tag inexistente para o cliente é no-op. Devolve status
        individual por empresa.

        **Trial:** escrita de tags é indisponível para chaves `is_trial` (requer `tags:write`) → **403**.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApplyTagsRequest'
            examples:
              porCnpj:
                summary: Remove a tag "cliente" de uma empresa
                value:
                  empresas:
                    - cnpj: '47.960.950/0001-21'
                  tags: [cliente]
      responses:
        '200':
          description: Status por empresa (sucesso/erro individual)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApplyTagsResponse'
        '202':
          description: Lote grande — enfileirado para processamento assíncrono
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TagJobAccepted'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          description: Fonte de dados de tags indisponível
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'

  /v4/companies/tags/jobs/{id}:
    get:
      tags: [companies]
      operationId: getCompanyTagJob
      x-required-scope: tags:read
      summary: Status de um job de tags assíncrono
      description: |
        Progresso de um lote enfileirado (resposta 202 do POST/DELETE). O detalhe
        por empresa é gravado no índice e no bookkeeping conforme o job avança.

        **Trial:** indisponível para chaves `is_trial` (requer `tags:read`) → **403**.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Status do job
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TagJobStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Job não encontrado para o cliente
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '500':
          $ref: '#/components/responses/InternalServerError'

  /v4/companies/{cnpj}/people:
    get:
      tags: [companies]
      operationId: listCompanyPeople
      x-required-scope: companies:people
      summary: Org-chart paginado (decisores/colaboradores)
      description: |
        Lista paginada de decisores (`id_nivel_decisao >= 1`) ou colaboradores
        (`== 0`) da empresa, do índice dedicado `plat-busca_pessoa`. É o conjunto
        completo do bucket pago `contatosAvancados` (que no lookup dá só um preview).
        Paginação por página (`pagina`/`tamanho`); universo por empresa < 10k. CPF/dados
        pessoais sob LGPD.

        **Filtro por departamento:** `departamento` (opcional, multi-valor) recorta pelas
        pessoas cujo `classificacaoMacro` contém ao menos um dos valores pedidos; a resposta
        ecoa o filtro aplicado em `departamento` (`null` quando não informado). Sem o
        parâmetro, resposta e cobrança são as de sempre.

        **Cobrança (tokens):** a base do endpoint **mais** os itens (pessoas) entregues na
        página. Só cobra **quando a página traz pessoa** (página vazia/empresa sem gente =
        grátis). Re-paginar a mesma empresa dentro de 24h não recobra os itens já entregues;
        com `departamento`, a dedup é por recorte — cada conjunto de departamentos cobra as
        pessoas que entrega, e repetir o mesmo conjunto em 24h não recobra.
        Saldo insuficiente → **402**.
      parameters:
        - $ref: '#/components/parameters/XEstimateOnly'
        - name: cnpj
          in: path
          required: true
          schema: { type: string }
          description: CNPJ com ou sem máscara (normalizado no servidor).
          example: '00000000000191'
        - name: papel
          in: query
          required: false
          schema:
            type: string
            enum: [decisores, colaboradores]
            default: decisores
        - name: pagina
          in: query
          required: false
          schema: { type: integer, minimum: 1, default: 1 }
        - name: tamanho
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - name: departamento
          in: query
          required: false
          style: form
          explode: true
          description: |
            Filtra por departamento (`classificacaoMacro`); vários valores fazem OR — repetido
            (`departamento=TI&departamento=Financeiro`) ou CSV (`departamento=TI,Financeiro`).
            Caixa, acentos e espaços são tolerados no servidor (`ti`, `juridico`); a resposta ecoa o
            label canônico. Valor fora do catálogo → **422** listando os aceitos. O recorte entra na
            chave de dedup da cobrança: cada conjunto de departamentos cobra as pessoas que entrega;
            repetir o mesmo conjunto em 24h não recobra.
          schema:
            type: array
            items:
              type: string
              enum:
                - Administrativo
                - Atendimento
                - Comercial
                - Compras
                - Financeiro
                - Jurídico
                - Marketing
                - Outros
                - Recursos Humanos
                - Sócio/Proprietário
                - Técnico
                - TI
          example: [TI]
      responses:
        '200':
          description: Página de pessoas
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PeoplePage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          description: Fonte de dados de pessoas indisponível
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
  /v4/companies/economic-group:
    post:
      tags: [companies]
      operationId: economicGroup
      x-required-scope: companies:economic_group
      summary: Empresas de 1ª ordem por sócios (experimental)
      description: |
        Dado um CNPJ, retorna as empresas de **1ª ordem** ligadas a ele — as empresas
        das quais os **sócios** da empresa pesquisada também são sócios. Lê o grafo de
        sócios direto no Elastic (`plat-socios-empresas` → `plat-socios-pf`), **deduplica
        por CNPJ matriz**, **exclui a própria empresa**, **ordena por porte**
        (GRANDE → MEDIO → PEQUENO → MICRO, desempate por CNPJ) e **pagina por offset**.

        **Cobrança (tokens):** igual ao `search_list` — a base do endpoint (pela
        segmentação) mais os campos entregues por empresa. Re-acessar a mesma empresa
        dentro de 24h não recobra. Saldo insuficiente → **402**.
      parameters:
        - $ref: '#/components/parameters/XEstimateOnly'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EconomicGroupRequest'
            examples:
              primeiraPagina:
                summary: Primeira página (20 empresas)
                value:
                  cnpj: '34938137000103'
                  pagina: 1
                  tamanho: 20
      responses:
        '200':
          description: Página de empresas relacionadas
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EconomicGroupResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '404':
          description: CNPJ não encontrado na base de sócios
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '500':
          $ref: '#/components/responses/InternalServerError'
        '503':
          description: Base de sócios indisponível
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
  /v4/account/balance:
    get:
      tags: [account]
      operationId: getAccountBalance
      x-required-scope: account:read
      summary: Saldo de tokens do cliente
      description: |
        Saldo de tokens disponível da conta (créditos menos débitos da carteira de tokens).
        Consulta somente leitura, nunca cobra.
      responses:
        '200':
          description: Saldo de tokens do cliente
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Balance'
              example:
                saldoTokens: 9013500
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalServerError'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: >-
        API key da v4 (`ek_live_`) enviada no header
        `Authorization: Bearer ek_live_...`. Emitida pelo staff via endpoint
        interno (fora deste contrato público). O modo sandbox `ek_test_` foi
        desabilitado — chaves `ek_test_` são rejeitadas (401).


        A chave pode ter **escopos por capacidade** (privilégio mínimo, ex.: uma
        chave de MCP só `companies:read,companies:match`). Cada operação declara o
        escopo exigido na extensão `x-required-scope`; a chave sem ele recebe **403**.
        Chaves legadas (escopo `api`) têm acesso total (wildcard, retrocompat).

  parameters:
    XEstimateOnly:
      name: X-Estimate-Only
      in: header
      required: false
      description: >-
        Dry-run / preview de custo (para agentes e MCP). `true` (ou `1`) faz o
        endpoint resolver a PRESENÇA dos campos, calcular o custo em tokens da
        chamada e devolver só `EstimateResponse` (`{ tokensEstimados }`) — **sem
        debitar e sem entregar dado pago**. O valor é o custo BRUTO (como se fosse
        a 1ª vez na janela; a dedup de 24h só barateia). Vale para todos os
        endpoints de dados (`search`, `companies`, `search_list(_by_id)`,
        `calc_list(_by_id)`, `match`, `people`, `economic-group`).
      schema:
        type: string
        enum: ['true', '1']

  responses:
    BadRequest:
      description: Payload malformado (JSON inválido, content-type errado, etc.)
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Unauthorized:
      description: Token ausente ou inválido
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    UnprocessableEntity:
      description: 'Payload semanticamente inválido (ex.: nenhum critério informado, pagina.tamanho fora do range)'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    PaymentRequired:
      description: 'Saldo insuficiente para a cobrança da operação — nenhum dado pago é entregue e nada é debitado'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    InternalServerError:
      description: Erro inesperado no servidor
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'

  schemas:
    # ---------- Preview de custo (dry-run) ----------
    EstimateResponse:
      type: object
      additionalProperties: false
      required: [tokensEstimados]
      description: >-
        Resposta do dry-run (`X-Estimate-Only`): custo BRUTO estimado em tokens da
        chamada, sem dado pago e sem débito.
      properties:
        tokensEstimados:
          type: integer
          minimum: 0
          description: Custo BRUTO estimado em tokens (teto; a dedup só barateia).
    # ---------- Match ----------
    Limite:
      type: integer
      minimum: 0
      description: |
        Cap de itens por lista entregue de **cada empresa** (telefones,
        e-mails, sócios, decisores, colaboradores, sites, redes, filiais). As
        listas saem **ordenadas por relevância**, então os N devolvidos são os N
        mais relevantes. Ausente → cap default (protege o custo, hoje **5**);
        `0` = traz todos e cobra tudo. Não afeta a paginação (nº de empresas por
        página) — corta só o que vem *dentro* de cada empresa.
      example: 5

    LimitePorLista:
      type: object
      additionalProperties: false
      description: |
        Override do `limite` por lista específica, ex.: `{ "telefones": 10,
        "socios": 3 }`. Valor inteiro >= 0 (`0` = todos). Chave desconhecida ou
        valor inválido → 422.
      properties:
        telefones:
          type: integer
          minimum: 0
        emailsValidadosSetores:
          type: integer
          minimum: 0
        emailsPublicos:
          type: integer
          minimum: 0
        socios:
          type: integer
          minimum: 0
        decisores:
          type: integer
          minimum: 0
        colaboradores:
          type: integer
          minimum: 0
        websites:
          type: integer
          minimum: 0
        redesSociais:
          type: integer
          minimum: 0
        filiais:
          type: integer
          minimum: 0
      example:
        telefones: 10
        socios: 3

    MatchRequest:
      type: object
      additionalProperties: false
      required: [criterios]
      properties:
        criterios:
          $ref: '#/components/schemas/MatchCriteria'
        incluir:
          $ref: '#/components/schemas/IncludeArray'
        campos:
          $ref: '#/components/schemas/FieldsArray'
        limite:
          $ref: '#/components/schemas/Limite'
        limitePorLista:
          $ref: '#/components/schemas/LimitePorLista'

    MatchCriteria:
      type: object
      additionalProperties: false
      description: |
        Conjunto de critérios para match. Pelo menos um campo deve estar presente
        e não-vazio — validado no servidor (retorna 422 se vazio).
      minProperties: 1
      properties:
        cnpj:
          type: string
          description: CNPJ em qualquer formato (com ou sem máscara)
          example: '47.960.950/0001-21'
        nome:
          type: string
          minLength: 1
          example: Magazine Luiza
        uf:
          type: string
          minLength: 2
          maxLength: 2
          pattern: '^[A-Z]{2}$'
          example: SP
        site:
          type: string
          format: hostname
          example: magazineluiza.com.br
        telefone:
          type: string
          description: >-
            Telefone em qualquer formato (E.164, nacional, com ou sem máscara).
            É normalizado a dígitos no servidor (remove máscara, espaços e o DDI
            +55) e casado de forma **exata** contra o telefone da empresa.
          example: '+5511999999999'
        email:
          type: string
          format: email
          example: contato@magazineluiza.com.br

    MatchResponse:
      type: object
      additionalProperties: false
      required: [correspondencias]
      description: |
        Top-N (até 5) melhores matches, ordenados por score (melhor primeiro).
        Lista vazia = nada encontrado (grátis). O melhor (`correspondencias[0]`) vem com
        `empresa` (núcleo + buckets do `incluir`) e é cobrado **quando confiante**;
        os demais são só identificação (desambiguação — para enriquecer, chame
        `POST/GET /v4/companies/search` com o CNPJ escolhido).
      properties:
        correspondencias:
          type: array
          maxItems: 5
          items:
            $ref: '#/components/schemas/MatchItem'

    MatchItem:
      type: object
      additionalProperties: false
      required: [cnpj, confianca, correspondidoEm, razaoSocial, nomeFantasia]
      description: |
        Item da lista de match. `confianca` = qualidade do match deste candidato
        (similaridade de nome, 0..1). `empresa` presente só no melhor item, quando
        confiante (único cobrado).
      properties:
        cnpj:
          type: string
          example: '47.960.950/0001-21'
        confianca:
          type: number
          format: double
          minimum: 0
          maximum: 1
          example: 0.92
        correspondidoEm:
          type: array
          minItems: 1
          items:
            type: string
            enum: [cnpj, nome, uf, site, telefone, email]
          example: [nome]
        razaoSocial:
          type: string
          nullable: true
          example: MAGAZINE LUIZA S/A
        nomeFantasia:
          type: string
          nullable: true
          example: Magazine Luiza
        empresa:
          $ref: '#/components/schemas/CompanyView'

    # ---------- Lookup (search) ----------
    CompanyLookupRequest:
      type: object
      additionalProperties: false
      required: [criterios]
      properties:
        criterios:
          $ref: '#/components/schemas/CompanyLookupCriteria'
        incluir:
          $ref: '#/components/schemas/IncludeArray'
        escopo:
          $ref: '#/components/schemas/Scope'
        campos:
          $ref: '#/components/schemas/FieldsArray'
        limite:
          $ref: '#/components/schemas/Limite'
        limitePorLista:
          $ref: '#/components/schemas/LimitePorLista'

    CompanyLookupCriteria:
      type: object
      additionalProperties: false
      description: |
        Identificadores fortes. Pelo menos um deve estar presente e não-vazio
        (422 se vazio).
      minProperties: 1
      properties:
        cnpj:
          type: string
          description: CNPJ com ou sem máscara — normalizado no servidor (apenas dígitos).
          example: '47.960.950/0001-21'
        site:
          type: string
          description: Site/domínio (com ou sem protocolo/www — normalizado no servidor)
          example: econodata.com.br
        email:
          type: string
          format: email
          example: contato@econodata.com.br
        raizCnpj:
          type: string
          description: Raiz do CNPJ (8 primeiros dígitos), com ou sem máscara — normalizado no servidor.
          example: '47960950'

    CompanyLookupResponse:
      type: object
      additionalProperties: false
      required: [empresa]
      properties:
        empresa:
          $ref: '#/components/schemas/CompanyView'

    CompaniesBatchRequest:
      type: object
      additionalProperties: false
      required: [cnpjs]
      properties:
        cnpjs:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: string
            description: CNPJ com ou sem máscara — normalizado no servidor.
          example: ['47960950000121', '11.111.111/1111-11']
        incluir:
          $ref: '#/components/schemas/IncludeArray'
        campos:
          $ref: '#/components/schemas/FieldsArray'
        limite:
          $ref: '#/components/schemas/Limite'
        limitePorLista:
          $ref: '#/components/schemas/LimitePorLista'

    CompaniesBatchResponse:
      type: object
      additionalProperties: false
      required: [empresas, erros]
      properties:
        empresas:
          type: array
          items:
            $ref: '#/components/schemas/CompanyView'
        erros:
          type: array
          items:
            $ref: '#/components/schemas/CompanyBatchError'

    CompanyBatchError:
      type: object
      additionalProperties: false
      required: [cnpj, motivo]
      properties:
        cnpj:
          type: string
          example: '11.111.111/1111-11'
        motivo:
          type: string
          example: not_found

    # ---------- Tags ----------
    ApplyTagsRequest:
      type: object
      additionalProperties: false
      required: [empresas, tags]
      properties:
        empresas:
          type: array
          minItems: 1
          maxItems: 100
          items:
            $ref: '#/components/schemas/CompanyTagTarget'
        tags:
          type: array
          minItems: 1
          maxItems: 20
          uniqueItems: true
          items:
            type: string
            minLength: 1
          example: [cliente, priority]

    CompanyTagTarget:
      type: object
      additionalProperties: false
      description: |
        Empresa-alvo. Pelo menos um identificador (cnpj, site ou email) é
        obrigatório — validado no servidor (422 se ausente). `marcarMatrizFiliais`
        sozinho não identifica empresa.
      minProperties: 1
      properties:
        cnpj:
          type: string
          description: CNPJ com ou sem máscara — normalizado no servidor.
          example: '47.960.950/0001-21'
        site:
          type: string
          description: Site/domínio (com ou sem protocolo/www — normalizado no servidor).
          example: empresa.com.br
        email:
          type: string
          format: email
          example: contato@empresa.com.br
        marcarMatrizFiliais:
          type: boolean
          default: false
          description: Propaga a marcação para todo o grupo Matriz-Filiais (mesma raiz de CNPJ).

    ApplyTagsResponse:
      type: object
      additionalProperties: false
      required: [resultados]
      properties:
        resultados:
          type: array
          items:
            $ref: '#/components/schemas/CompanyTagResult'

    TagJobAccepted:
      type: object
      additionalProperties: false
      required: [idJob, urlStatus]
      properties:
        idJob:
          type: string
          format: uuid
          example: 7d9c2a10-4b1e-4c3a-9f2e-1a2b3c4d5e6f
        urlStatus:
          type: string
          example: /v4/companies/tags/jobs/7d9c2a10-4b1e-4c3a-9f2e-1a2b3c4d5e6f

    TagJobStatus:
      type: object
      additionalProperties: false
      required: [id, status, processados, falhas]
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [pending, running, done, failed]
        total:
          type: integer
          nullable: true
          description: Total de empresas (pode ser null enquanto o job avança).
        processados:
          type: integer
        falhas:
          type: integer

    CompanyTagResult:
      type: object
      additionalProperties: false
      required: [sucesso, cnpj, mensagem]
      properties:
        sucesso:
          type: boolean
          example: true
        cnpj:
          type: string
          description: CNPJ da empresa marcada, ou o identificador informado quando não encontrada.
          example: '47.960.950/0001-21'
        mensagem:
          type: string
          example: Tags adicionadas com sucesso.

    CompanyView:
      type: object
      additionalProperties: false
      required: [cnpj]
      properties:
        cnpj:
          type: string
          example: '00.000.000/0001-91'
        cadastro:
          $ref: '#/components/schemas/CompanyCadastro'
        estrategico:
          $ref: '#/components/schemas/CompanyEstrategico'
        perfilNegocio:
          $ref: '#/components/schemas/CompanyPerfilNegocio'
        contatosBasicos:
          $ref: '#/components/schemas/CompanyContatosBasicos'
        contatosAvancados:
          $ref: '#/components/schemas/CompanyContatosAvancados'

    # ---------- Search (segmentação) ----------
    SearchRequest:
      type: object
      additionalProperties: false
      required: [filtros]
      description: |
        Segmentação por **filtros diretos** (`filtros` obrigatório). Paginação por
        cursor opaco. A segmentação por pesquisa salva é um endpoint à parte (futuro).
      properties:
        filtros:
          $ref: '#/components/schemas/SearchFilters'
        pagina:
          $ref: '#/components/schemas/PageRequest'
        incluir:
          $ref: '#/components/schemas/IncludeArray'
        campos:
          $ref: '#/components/schemas/FieldsArray'
        limite:
          $ref: '#/components/schemas/Limite'
        limitePorLista:
          $ref: '#/components/schemas/LimitePorLista'

    SearchByIdRequest:
      type: object
      additionalProperties: false
      required: [nome]
      description: |
        Segmentação por **pesquisa salva** (`nome` obrigatório — o nome exato da
        pesquisa em `/#/prospeccao/pesquisas`). Mesma paginação/`incluir`/`campos`
        do `search_list`; os filtros vêm da pesquisa salva (não do corpo).
      properties:
        nome:
          type: string
          minLength: 1
          description: Nome exato da pesquisa salva (escopo compartilhado, único na conta).
          example: Bancos de São Paulo
        pagina:
          $ref: '#/components/schemas/PageRequest'
        incluir:
          $ref: '#/components/schemas/IncludeArray'
        campos:
          $ref: '#/components/schemas/FieldsArray'
        limite:
          $ref: '#/components/schemas/Limite'
        limitePorLista:
          $ref: '#/components/schemas/LimitePorLista'

    SearchFilters:
      type: object
      additionalProperties: false
      description: |
        Filtros diretos de segmentação — todos opcionais e combináveis. Os nomes e o
        formato dos valores seguem o **contrato de saída** (camelCase PT; enums
        MAIÚSCULOS), os mesmos campos que o cliente vê na resposta `CompanyView`.
      properties:
        situacao:
          type: array
          description: 'core.situacao: ATIVA / BAIXADA / INAPTA / SUSPENSA / NULA.'
          items: { type: string }
        dataAbertura:
          allOf: [{ $ref: '#/components/schemas/FaixaData' }]
          description: 'profile.dataAbertura — faixa ISO-8601.'
        uf:
          type: array
          description: 'core.uf (ex.: ["SP"]).'
          items: { type: string }
        cidade:
          type: array
          description: 'core.cidade.'
          items: { type: string }
        bairro:
          type: array
          description: 'profile.endereco.bairro.'
          items: { type: string }
        cep:
          type: string
          minLength: 1
          description: 'profile.endereco.cep (com ou sem máscara).'
        mesoRegiao:
          type: array
          description: 'Mesorregião — código IBGE (filtro-only; sem equivalente na resposta).'
          items: { type: string }
        microRegiao:
          type: array
          description: 'Microrregião — código IBGE (filtro-only; sem equivalente na resposta).'
          items: { type: string }
        cnaePrimario:
          type: array
          description: 'profile.cnaePrimario.codigo — código CNAE completo ou prefixo.'
          items: { type: string }
        setores:
          type: array
          description: 'profile.setores (setor amigável).'
          items: { type: string }
        porte:
          type: array
          description: 'perfilNegocio.porte: INDIVIDUAL/MICRO/PEQUENO/MEDIO/GRANDE.'
          items: { type: string }
        enquadramentoPorte:
          type: array
          description: 'profile.enquadramentoPorte: ME/EPP/MEI/Sem Enquadramento.'
          items: { type: string }
        regimeTributario:
          type: array
          description: 'profile.regimeTributario.atual (ex.: ["SIMPLES NACIONAL"]).'
          items: { type: string }
        capitalSocial:
          allOf: [{ $ref: '#/components/schemas/Faixa' }]
          description: 'profile.capitalSocial — faixa em reais.'
        funcionarios:
          allOf: [{ $ref: '#/components/schemas/Faixa' }]
          description: 'cnpjSize.funcionarios — faixa (nível CNPJ).'
        faturamento:
          allOf: [{ $ref: '#/components/schemas/Faixa' }]
          description: 'cnpjSize.faturamento — faixa em reais (nível CNPJ).'
        tipoUnidade:
          type: string
          enum: [MATRIZ, FILIAL]
          description: 'core.tipoUnidade.'
        palavrasChave:
          allOf: [{ $ref: '#/components/schemas/PalavrasChave' }]
          description: >-
            Termos a incluir/excluir e os escopos de busca (filtro-only; sem equivalente
            na resposta).
        cnpjs:
          type: array
          minItems: 1
          maxItems: 1000
          description: >-
            core.cnpj (14 posições alfanuméricas) e/ou businessGroup.raizCnpj (8) — máscara
            opcional. A raiz traz todas as unidades do grupo.
          items: { type: string }
        nivelAtividade:
          type: array
          description: >-
            cadastro.nivelAtividade — soma dos níveis pedidos (não é faixa: cada valor
            entra exatamente como pedido).
          items:
            type: string
            enum: [ALTA, MEDIA, BAIXA, INATIVA]
        situacaoCrescimentoSetor:
          type: array
          description: 'businessGroup.situacaoCrescimentoSetor.'
          items:
            type: string
            enum:
              - EM FORTE CRESCIMENTO
              - EM CRESCIMENTO
              - EM ESTABILIDADE
              - EM RETRACAO
              - EM FORTE RETRACAO
        comTelefones:
          type: boolean
          description: 'Só empresas com telefone (filtro-only; `false` não filtra nada).'
        tipoTelefones:
          type: array
          description: >-
            contatosBasicos.telefones[].tipo — AND entre os tipos: a empresa precisa ter um
            telefone de cada tipo pedido.
          items:
            type: string
            enum: [FIXO, CELULAR, ATENDIMENTO]
        comEmailsValidados:
          type: boolean
          description: 'Só empresas com e-mail validado (filtro-only; `false` não filtra nada).'
        comEmailsPublicos:
          type: boolean
          description: >-
            Só empresas com e-mail público (site) ou da Receita (filtro-only; `false` não
            filtra nada).
        cnaeSecundario:
          type: array
          description: 'profile.cnaesSecundarios[].codigo — código CNAE completo ou prefixo.'
          items: { type: string }
        assertividadeTelefones:
          allOf: [{ $ref: '#/components/schemas/FaixaAssertividade' }]
          description: 'contatosAvancados.assertividadeTelefones — faixa 0 a 5.'
        raio:
          allOf: [{ $ref: '#/components/schemas/Raio' }]
          description: 'Ponto + raio em km (filtro-only; combinado com os demais filtros).'

    PalavrasChave:
      type: object
      additionalProperties: false
      description: >-
        Palavras-chave da segmentação. `em` escolhe os escopos de busca; ausente = busca no
        nome da empresa E nos produtos/serviços.
      properties:
        incluir:
          type: array
          maxItems: 20
          items: { type: string, minLength: 2, maxLength: 100 }
        excluir:
          type: array
          maxItems: 20
          items: { type: string, minLength: 2, maxLength: 100 }
        em:
          type: array
          minItems: 1
          items:
            type: string
            enum: [NOME_EMPRESA, PRODUTOS_SERVICOS]

    Raio:
      type: object
      additionalProperties: false
      required: [latitude, longitude, km]
      description: Ponto geográfico e raio de busca em km.
      properties:
        latitude: { type: number, minimum: -90, maximum: 90, example: -25.43 }
        longitude: { type: number, minimum: -180, maximum: 180, example: -49.27 }
        km: { type: number, minimum: 1, maximum: 200, example: 20 }

    FaixaAssertividade:
      type: object
      additionalProperties: false
      description: Faixa de assertividade dos telefones (0 a 5; limites opcionais).
      properties:
        de: { type: integer, minimum: 0, maximum: 5, nullable: true }
        ate: { type: integer, minimum: 0, maximum: 5, nullable: true }

    Faixa:
      type: object
      additionalProperties: false
      description: Faixa numérica (limites opcionais; null = aberto).
      properties:
        de: { type: integer, nullable: true }
        ate: { type: integer, nullable: true }

    FaixaData:
      type: object
      additionalProperties: false
      description: Faixa de datas ISO-8601 (limites opcionais; null = aberto).
      properties:
        de: { type: string, format: date, nullable: true }
        ate: { type: string, format: date, nullable: true }

    SearchResponse:
      type: object
      additionalProperties: false
      required: [totalRegistros, pagina, resultados]
      description: |
        Página de resultados. **Sempre cobra** a base do endpoint + os campos
        entregues por empresa; não há modalidade gratuita (`calc_list` devolve só a
        contagem sem cobrar por campo).
      properties:
        totalRegistros:
          type: integer
          minimum: 0
          example: 4827
        pagina:
          $ref: '#/components/schemas/SearchPageResponse'
        resultados:
          type: array
          items:
            $ref: '#/components/schemas/CompanyView'

    CalcResponse:
      type: object
      additionalProperties: false
      required: [totalRegistros]
      description: |
        Contagem total do conjunto (`calc_list` / `calc_list_by_id`) — só o número de
        empresas que casam a busca, sem empresas nem paginação. Cobra a base (100),
        sem cobrança por campo.
      properties:
        totalRegistros:
          type: integer
          minimum: 0
          example: 4827

    SavedSearchSummary:
      type: object
      additionalProperties: false
      required: [nome, criadaEm, searchId]
      description: |
        Metadado de uma pesquisa salva compartilhada. `nome` é o valor a enviar em
        `search_list_by_id`/`calc_list_by_id`; `searchId` (o `url_param` estável)
        serve só para desambiguar nomes repetidos na UI.
      properties:
        nome:
          type: string
          example: Bancos de São Paulo
        criadaEm:
          type: string
          format: date-time
          example: '2026-05-14T13:22:41.000Z'
        searchId:
          type: string
          example: 7b2f1a9c4d
    SavedSearchesResponse:
      type: object
      additionalProperties: false
      required: [pesquisasSalvas]
      description: |
        Pesquisas salvas compartilhadas da conta (`GET /v4/companies/saved_searches`),
        deduplicadas por nome (mais recente) e ordenadas por criação desc. Metadado —
        não cobra tokens.
      properties:
        pesquisasSalvas:
          type: array
          items:
            $ref: '#/components/schemas/SavedSearchSummary'

    PageRequest:
      type: object
      additionalProperties: false
      properties:
        tamanho:
          type: integer
          minimum: 1
          maximum: 200
          default: 50
          example: 50
        cursor:
          type: string
          nullable: true
          description: Cursor opaco devolvido em `cursorProximo` da página anterior. `null` para primeira página.

    SearchPageResponse:
      type: object
      additionalProperties: false
      required: [tamanho]
      properties:
        tamanho:
          type: integer
          minimum: 1
          example: 50
        cursorProximo:
          type: string
          nullable: true
          description: Cursor para a próxima página, ou `null` se for a última.
          example: Y3Vyc29yOjUw

    # ---------- Compartilhados ----------
    IncludeArray:
      type: array
      description: |
        Buckets opcionais a expandir na resposta (5 buckets por categoria de valor). Ver
        `docs/negocio/contratos-novos-endpoints-v4.md`.
      uniqueItems: true
      items:
        type: string
        enum: [cadastro, estrategico, perfilNegocio, contatosBasicos, contatosAvancados]
      example: [cadastro, contatosBasicos]

    FieldsArray:
      type: array
      description: |
        Seleção de folhas por dot-path no formato `bucket.campo` (ex.:
        `estrategico.faturamentoCnpj.valor`). Auto-inclui o bucket da folha e **define o
        custo** — cobra só os campos entregues. Omisso → tudo dos buckets pedidos.
        Path que não existe no contrato → **422** listando o que corrigir (não é ignorado
        em silêncio, que devolveria só o `cnpj` cobrando a base do endpoint).
      uniqueItems: true
      items:
        type: string
        minLength: 1
      example: [cadastro.razaoSocial, contatosBasicos.telefones]

    Scope:
      type: string
      description: |
        Recorte de unidade: `unidade` (padrão, só a unidade), `filiais` ou `grupo`
        (matriz + filiais da mesma raiz, em `perfilNegocio.filiais`).
      enum: [unidade, filiais, matrizFiliais]
      default: unidade

    # ---------- Buckets do CompanyView (ver contratos-novos-endpoints-v4.md) ----------
    CompanyCadastro:
      type: object
      additionalProperties: false
      description: 'Cadastral RFB + QSA (incluir=cadastro). Inclui razaoSocial/nomeFantasia — v4.1 cobrados aqui, não mais identidade grátis no topo.'
      properties:
        razaoSocial: { type: string, nullable: true, example: BANCO DO BRASIL SA }
        nomeFantasia: { type: string, nullable: true, example: DIRECAO GERAL }
        tipoUnidade: { type: string, nullable: true, enum: [MATRIZ, FILIAL] }
        situacao: { type: string, nullable: true, enum: [ATIVA, BAIXADA, INAPTA, SUSPENSA, NULA] }
        uf: { type: string, nullable: true }
        cidade: { type: string, nullable: true }
        melhorNomeInferido: { type: string, nullable: true }
        setores:
          type: array
          items: { type: string }
        cnaePrimario: { $ref: '#/components/schemas/CnaeRef' }
        cnaesSecundarios:
          type: array
          items: { $ref: '#/components/schemas/AtividadeSecundaria' }
        endereco: { $ref: '#/components/schemas/Endereco' }
        naturezaJuridica: { $ref: '#/components/schemas/NaturezaJuridica' }
        situacaoCadastral: { $ref: '#/components/schemas/SituacaoCadastral' }
        regimeTributario: { $ref: '#/components/schemas/RegimeTributario' }
        situacaoSimples: { $ref: '#/components/schemas/Simples' }
        enquadramentoPorte:
          type: string
          nullable: true
          enum: [ME, EPP, MEI, 'Sem Enquadramento']
          example: Sem Enquadramento
        dataAbertura: { type: string, nullable: true, example: '1966-08-01T00:00:00Z' }
        capitalSocial: { type: number, nullable: true, example: 12000000000 }
        nivelAtividade:
          type: string
          nullable: true
          enum: [alta, media, baixa, inativa]
          description: Classificação do nível de atividade — metadado, não cobrado.
        socios:
          type: array
          items: { $ref: '#/components/schemas/Socio' }

    CnaeRef:
      type: object
      additionalProperties: false
      properties:
        codigo: { type: string, nullable: true, example: 'K-6422-1/00' }
        descricao: { type: string, nullable: true }

    Endereco:
      type: object
      additionalProperties: false
      properties:
        logradouro: { type: string, nullable: true }
        bairro: { type: string, nullable: true }
        cidade: { type: string, nullable: true }
        uf: { type: string, nullable: true }
        cep: { type: string, nullable: true, example: '70.040-912' }

    NaturezaJuridica:
      type: object
      additionalProperties: false
      properties:
        codigo: { type: string, nullable: true, example: '203-8' }
        nome: { type: string, nullable: true }

    SituacaoCadastral:
      type: object
      additionalProperties: false
      properties:
        tipo: { type: string, nullable: true, example: ATIVA }
        data: { type: string, nullable: true }
        motivo: { type: string, nullable: true }
        tipoEspecial: { type: string, nullable: true }
        dataEspecial: { type: string, nullable: true }

    RegimeTributario:
      type: object
      additionalProperties: false
      properties:
        anterior: { type: string, nullable: true }
        atual: { type: string, nullable: true, example: LUCRO REAL }

    Simples:
      type: object
      additionalProperties: false
      properties:
        situacao: { type: string, nullable: true, example: EXCLUIDO }
        dataOpcao: { type: string, nullable: true }
        dataExclusao: { type: string, nullable: true }
        mei: { type: boolean, nullable: true }

    AtividadeSecundaria:
      type: object
      additionalProperties: false
      properties:
        cnae: { type: string, nullable: true }
        descricao: { type: string, nullable: true }

    CompanyEstrategico:
      type: object
      additionalProperties: false
      description: Dimensionamento (headcount/faturamento CNPJ e grupo) + tecnologias (incluir=estrategico).
      properties:
        headcountCnpj: { $ref: '#/components/schemas/CnpjSizeMetric' }
        faturamentoCnpj: { $ref: '#/components/schemas/CnpjSizeMetric' }
        headcountMatrizFiliais: { $ref: '#/components/schemas/GrupoFuncionarios' }
        faturamentoMatrizFiliais: { $ref: '#/components/schemas/GrupoFaturamento' }
        tecnologias: { $ref: '#/components/schemas/Tecnologias' }

    CnpjSizeMetric:
      type: object
      additionalProperties: false
      description: 'Faixa {de,ate} + valor pontual + origem ("Presumido" = estimado).'
      properties:
        de: { type: number, nullable: true }
        ate: { type: number, nullable: true }
        valor: { type: number, nullable: true }
        origem: { type: string, nullable: true }

    CompanyPerfilNegocio:
      type: object
      additionalProperties: false
      description: Sinais de negócio + grupo Matriz-Filiais + dívidas + NCM (incluir=perfilNegocio).
      properties:
        porte:
          type: string
          nullable: true
          enum: [INDIVIDUAL, MICRO, PEQUENO, MEDIO, GRANDE]
          example: GRANDE
        recebimentosGoverno:
          type: number
          nullable: true
          description: Valor recebido do governo (licitações), em reais.
        comercioExterior: { $ref: '#/components/schemas/ComercioExterior' }
        situacaoCrescimentoSetor: { type: string, nullable: true }
        raizCnpj: { type: string, nullable: true, example: '00.000.000' }
        nome: { type: string, nullable: true }
        ufs:
          type: array
          items: { type: string }
        qtdFiliais:
          type: integer
          minimum: 0
          example: 6
          description: 'Número de filiais ativas da empresa, sem contar a matriz. Vale 0 quando não há filial (nunca null) e vem em qualquer escopo.'
        filiais:
          type: array
          description: 'Matriz + filiais da mesma raiz — presente só quando escopo=filiais|matrizFiliais.'
          items: { $ref: '#/components/schemas/Filial' }
        dividasResumoTipo: { $ref: '#/components/schemas/ResumoDividas' }
        dividasResumoTitulares: { $ref: '#/components/schemas/ResumoDividas' }
        detalhe:
          type: array
          items: { $ref: '#/components/schemas/Divida' }
        ncmsPrimarios:
          type: array
          items: { $ref: '#/components/schemas/Ncm' }

    Filial:
      type: object
      additionalProperties: false
      description: Unidade do grupo (núcleo) — só com escopo=filiais|matrizFiliais.
      required: [cnpj]
      properties:
        cnpj: { type: string, example: '00.000.000/0002-72' }
        razaoSocial: { type: string, nullable: true }
        nomeFantasia: { type: string, nullable: true }
        tipoUnidade: { type: string, nullable: true, enum: [MATRIZ, FILIAL] }
        situacao: { type: string, nullable: true, enum: [ATIVA, BAIXADA, INAPTA, SUSPENSA, NULA] }
        uf: { type: string, nullable: true }
        cidade: { type: string, nullable: true }

    GrupoFuncionarios:
      type: object
      additionalProperties: false
      description: 'Faixa sempre macro. origem = procedência ("Presumido" = estimado).'
      properties:
        total: { type: integer, nullable: true }
        de: { type: integer, nullable: true }
        ate: { type: integer, nullable: true }
        origem: { type: string, nullable: true }

    GrupoFaturamento:
      type: object
      additionalProperties: false
      description: 'Reais; faixa sempre macro. origem = procedência ("Presumido" = estimado).'
      properties:
        valor: { type: number, nullable: true }
        de: { type: number, nullable: true }
        ate: { type: number, nullable: true }
        origem: { type: string, nullable: true }

    ComercioExterior:
      type: object
      additionalProperties: false
      properties:
        exportacaoAnos:
          type: array
          items: { type: string }
        importacaoAnos:
          type: array
          items: { type: string }

    CompanyContatosBasicos:
      type: object
      additionalProperties: false
      description: Contato Simples — todos os telefones (v4.1) + e-mails (incluir=contatosBasicos).
      properties:
        telefones:
          type: array
          items: { $ref: '#/components/schemas/Telefone' }
        emailReceita: { type: string, nullable: true, example: secex@bb.com.br }
        emailsValidadosSetores:
          type: array
          description: E-mails validados de departamentos/setores.
          items: { type: string }
        emailsPublicos:
          type: array
          items: { $ref: '#/components/schemas/EmailPublico' }

    Telefone:
      type: object
      additionalProperties: false
      properties:
        numero: { type: string, nullable: true, example: '08009792020' }
        tipo: { type: string, nullable: true, example: atendimento }
        unidade:
          type: string
          nullable: true
          enum: [MATRIZ, FILIAL]
          description: Unidade à qual o telefone pertence.
        temWhatsapp: { type: boolean, nullable: true }
        assertividade:
          type: string
          nullable: true
          enum: [alta, media, baixa]
          example: alta
        verificado: { type: boolean, nullable: true }
        prioridade: { type: integer, nullable: true }

    CompanyContatosAvancados:
      type: object
      additionalProperties: false
      description: |
        Contato Avançado — decisores/colaboradores + web (incluir=contatosAvancados).
        v4.1: **sem telefones** (todos os telefones ficam em contatosBasicos). CPF
        mascarado (LGPD). `decisores`/`colaboradores` vêm capped (top-N por nível); o
        conjunto completo é paginado em `GET /v4/companies/{cnpj}/people`.
      properties:
        assertividadeTelefones:
          type: number
          nullable: true
          minimum: 0
          maximum: 5
          description: Resumo das assertividades de telefone (0–5) — de score_scaled; metadado, não cobrado.
        decisores: { $ref: '#/components/schemas/PessoaPage' }
        colaboradores: { $ref: '#/components/schemas/PessoaPage' }
        websites:
          type: array
          items: { $ref: '#/components/schemas/Website' }
        redesSociais:
          type: array
          items: { $ref: '#/components/schemas/RedeSocial' }

    PessoaPage:
      type: object
      additionalProperties: false
      description: 'Envelope de pessoas — capped no lookup (`temMais`), paginado no endpoint dedicado.'
      required: [total, temMais, itens]
      properties:
        total: { type: integer }
        temMais: { type: boolean }
        itens:
          type: array
          items: { $ref: '#/components/schemas/Pessoa' }

    Pessoa:
      type: object
      additionalProperties: false
      properties:
        nome: { type: string, nullable: true }
        cargos:
          type: array
          items: { type: string }
        tipo:
          type: string
          description: 'Derivado de id_nivel_decisao: "Decisor" (>= 1) | "Colaborador" (== 0).'
        nivelDecisao: { type: string, nullable: true }
        classificacaoMacro:
          type: array
          items: { type: string }
        classificacaoMicro:
          type: array
          items: { type: string }
        emails:
          type: array
          nullable: true
          description: 'E-mails da pessoa: os do cadastro dela e os que a Econodata já encontrou para esse nome nesta empresa. Nulo quando não há nenhum.'
          items: { type: string }
        urlFoto: { type: string, nullable: true }
        redesSociais:
          type: array
          items: { $ref: '#/components/schemas/RedeSocialPessoa' }
        dataDado: { type: string, nullable: true }

    RedeSocialPessoa:
      type: object
      additionalProperties: false
      properties:
        rede: { type: string, nullable: true }
        url: { type: string, nullable: true }

    PeoplePage:
      type: object
      additionalProperties: false
      description: Resposta paginada de `GET /v4/companies/{cnpj}/people`.
      required: [papel, departamento, total, pagina, tamanho, totalPaginas, itens]
      properties:
        papel: { type: string, enum: [decisores, colaboradores] }
        departamento:
          type: array
          nullable: true
          items: { type: string }
          description: Filtro aplicado (labels canônicos, na ordem informada) ou `null` quando não informado.
        total: { type: integer }
        pagina: { type: integer }
        tamanho: { type: integer }
        totalPaginas: { type: integer }
        itens:
          type: array
          items: { $ref: '#/components/schemas/Pessoa' }

    EconomicGroupRequest:
      type: object
      additionalProperties: false
      description: Corpo de `POST /v4/companies/economic-group`.
      required: [cnpj]
      properties:
        cnpj:
          type: string
          description: CNPJ da empresa pesquisada (com ou sem máscara; normalizado no servidor).
          example: '34938137000103'
        pagina: { type: integer, minimum: 1, default: 1 }
        tamanho: { type: integer, minimum: 1, maximum: 100, default: 20 }

    RelatedCompany:
      type: object
      additionalProperties: false
      description: Empresa relacionada de 1ª ordem (via sócios em comum).
      required: [cnpj]
      properties:
        cnpj: { type: string, example: '55.170.227/0001-77' }
        razaoSocial: { type: string, nullable: true }
        nomeFantasia: { type: string, nullable: true }
        cidade: { type: string, nullable: true }
        uf: { type: string, nullable: true, example: 'MA' }
        porte: { type: string, nullable: true, example: 'MEDIO' }

    EconomicGroupResponse:
      type: object
      additionalProperties: false
      description: Resposta paginada de `POST /v4/companies/economic-group`.
      required: [cnpj, totalRegistros, pagina, tamanho, temMais, empresas]
      properties:
        cnpj: { type: string, example: '34.938.137/0001-03' }
        totalRegistros: { type: integer }
        pagina: { type: integer }
        tamanho: { type: integer }
        temMais: { type: boolean }
        empresas:
          type: array
          items: { $ref: '#/components/schemas/RelatedCompany' }

    Socio:
      type: object
      additionalProperties: false
      properties:
        nome: { type: string, nullable: true }
        qualificacao: { type: string, nullable: true }
        cpf: { type: string, nullable: true, example: '***.550.179-**' }
        cnpjSocio: { type: string, nullable: true }
        email:
          type: string
          nullable: true
          description: 'E-mail do sócio: o registrado na Receita Federal ou, sem ele, o que a Econodata já encontrou para esse nome nesta empresa. Nulo quando não há.'
        dataEntrada: { type: string, nullable: true }
        representanteLegal: { type: string, nullable: true }

    EmailPublico:
      type: object
      additionalProperties: false
      properties:
        email: { type: string, nullable: true }
        origem: { type: string, nullable: true }
        prioridade: { type: integer, nullable: true }
        posicao: { type: integer, nullable: true }
        similaridade: { type: number, nullable: true }

    ResumoDividas:
      type: object
      additionalProperties: false
      properties:
        uniao: { $ref: '#/components/schemas/BlocoDivida' }
        uniaoFgts: { $ref: '#/components/schemas/BlocoDivida' }
        uniaoGerais: { $ref: '#/components/schemas/BlocoDivida' }
        uniaoPrevidenciarias: { $ref: '#/components/schemas/BlocoDivida' }

    BlocoDivida:
      type: object
      nullable: true
      additionalProperties: false
      properties:
        totalPrincipal: { type: number, nullable: true }
        qtdePrincipal: { type: integer, nullable: true }
        percentualFaturamentoPrincipal: { type: number, nullable: true }
        dividaMaisAntiga: { type: string, nullable: true }
        dividaMaisRecente: { type: string, nullable: true }

    Divida:
      type: object
      additionalProperties: false
      properties:
        credor: { type: string, nullable: true }
        cpfCnpj: { type: string, nullable: true }
        tipoDevedor: { type: string, nullable: true }
        tipoPessoa: { type: string, nullable: true }
        tipoSituacao: { type: string, nullable: true }
        dataInscricao: { type: string, nullable: true }
        valor: { type: number, nullable: true }
        probabilidade: { type: number, nullable: true }
        origem: { $ref: '#/components/schemas/DividaOrigem' }

    DividaOrigem:
      type: object
      nullable: true
      additionalProperties: false
      properties:
        remessa: { type: string, nullable: true }
        tipo: { type: string, nullable: true }

    Website:
      type: object
      additionalProperties: false
      properties:
        dominio: { type: string, nullable: true }
        prioridade: { type: integer, nullable: true }
        assertividade: { type: string, nullable: true }
        valido: { type: boolean, nullable: true }
        temMx: { type: boolean, nullable: true }

    RedeSocial:
      type: object
      additionalProperties: false
      properties:
        rede: { type: string, nullable: true }
        dominio: { type: string, nullable: true }
        posicao: { type: integer, nullable: true }

    Tecnologias:
      type: object
      additionalProperties: false
      properties:
        categorias:
          type: array
          items: { type: string }
        tecnologias:
          type: array
          items: { type: string }

    Ncm:
      type: object
      additionalProperties: false
      properties:
        codigo: { type: string, nullable: true }
        numero: { type: string, nullable: true }
        descricao: { type: string, nullable: true }

    # ---------- Account ----------
    Balance:
      type: object
      description: Saldo de tokens da conta.
      required: [saldoTokens]
      properties:
        saldoTokens:
          type: integer
          format: int64
          description: Tokens disponíveis (créditos menos débitos).
          example: 9013500

    Problem:
      type: object
      description: Erro no formato RFC 7807 (application/problem+json)
      required: [type, title, status]
      properties:
        type:
          type: string
          format: uri
          description: URI identificando o tipo do erro
          example: https://errors.econodata.com.br/validation
        title:
          type: string
          example: Validation failed
        status:
          type: integer
          minimum: 400
          maximum: 599
          example: 422
        detail:
          type: string
          example: criterios deve ter ao menos um campo preenchido
        instance:
          type: string
          description: 'Identificador único da ocorrência (ex.: requestId)'
          example: 1f1d0c3e-...
