openapi: 3.1.0
info:
  title: notta API
  version: v1
  description: API headless da notta para emissão de NFS-e Nacional (DPS), gestão de clientes e webhooks de eventos. Documentação pt-BR, envelope canônico `{ data }` / `{ error }`, autenticação via token `nta_live_...`. Ver Quickstart em `/docs/api` para exemplos práticos.
  contact:
    name: notta
    url: https://notta.app
  license:
    name: Proprietary
servers:
  - url: https://api.notta.app
    description: Produção
  - url: https://sandbox.notta.app
    description: Sandbox (Epic 6 — provisioning ainda não disponível; use produção com CNPJ homologação).
tags:
  - name: Notas
    description: Emissão, listagem, cancelamento, reemissão, consulta e reenvio de NFS-e.
  - name: Clientes
    description: Cadastro de tomadores de serviço.
  - name: Contatos
    description: Contatos financeiros (email/WhatsApp) do Cliente para distribuição de NFS-e.
  - name: Cobranças
    description: Links de pagamento reutilizáveis (via form público) que emitem NFS-e ao serem consumidos.
  - name: Forms
    description: Forms públicos de emissão de NFS-e (Epic 4).
  - name: Configuração
    description: Configuração NFS-e do prestador (regime, certificado A1, email, WhatsApp).
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: nta_live_{random32}
      description: "Token de API (`Authorization: Bearer nta_live_...`). Gere em `/app/api/tokens` (Story 5.2). Escopo obrigatório por endpoint — ver descrição de cada rota."
  schemas:
    DpsInput:
      type: object
      properties:
        cliente_id:
          type: string
          format: uuid
        servico_id:
          type: string
          format: uuid
        parcela_id:
          type: string
          format: uuid
        servico_recorrente_id:
          type: string
          format: uuid
        tomador_cnpj:
          type: string
          minLength: 14
          maxLength: 18
        tomador_cpf:
          type: string
          minLength: 11
          maxLength: 14
        tomador_nome:
          type: string
          minLength: 1
        tomador_razao_social:
          type: string
          minLength: 1
        tomador_logradouro:
          type: string
        tomador_numero:
          type: string
        tomador_bairro:
          type: string
        tomador_municipio:
          type: string
        tomador_uf:
          type: string
          minLength: 2
          maxLength: 2
        tomador_cep:
          type: string
        tomador_email:
          anyOf:
            - type: string
              format: email
            - type: string
              enum:
                - ""
        tomador_telefone:
          type: string
        valor:
          type: number
          exclusiveMinimum: 0
        descricao_servico:
          type: string
          minLength: 1
        codigo_servico:
          type: string
        aliquota_iss:
          type: number
          minimum: 0
          maximum: 100
      required:
        - cliente_id
        - valor
        - descricao_servico
      description: Payload de emissão de NFS-e. Aceita campos mínimos do tomador ou apenas `cliente_id` (denormaliza do cadastro).
    CancelarNfseInput:
      type: object
      properties:
        motivo:
          type: string
          minLength: 15
        codigoMotivo:
          type: string
          enum:
            - "1"
            - "2"
            - "9"
      required:
        - motivo
        - codigoMotivo
      description: Motivo de cancelamento + código municipal (1=erro emissão, 2=serviço não prestado, 9=outros).
    ClienteCreate:
      type: object
      properties:
        cnpj:
          type: string
          minLength: 14
          maxLength: 18
        cpf:
          type: string
          minLength: 11
          maxLength: 14
        razao_social:
          type: string
          minLength: 1
          maxLength: 255
        nome_fantasia:
          type: string
          maxLength: 255
        endereco:
          type: object
          properties:
            logradouro:
              type: string
              minLength: 1
            numero:
              type: string
              minLength: 1
            complemento:
              type: string
            bairro:
              type: string
              minLength: 1
            municipio:
              type: string
              minLength: 1
            uf:
              type: string
              minLength: 2
              maxLength: 2
            cep:
              type: string
              minLength: 8
              maxLength: 9
            codigoMunicipio:
              type: string
          required:
            - logradouro
            - numero
            - bairro
            - municipio
            - uf
            - cep
        inscricao_estadual:
          type: string
          maxLength: 50
        email:
          anyOf:
            - type: string
              format: email
            - type: string
              enum:
                - ""
        telefone:
          type: string
          maxLength: 20
      required:
        - razao_social
        - endereco
      description: Payload de criação de Cliente (tomador de NFS-e). CNPJ único por tenant.
    ClienteUpdate:
      type: object
      properties:
        cnpj:
          type: string
          minLength: 14
          maxLength: 18
        cpf:
          type: string
          minLength: 11
          maxLength: 14
        razao_social:
          type: string
          minLength: 1
          maxLength: 255
        nome_fantasia:
          type: string
          maxLength: 255
        endereco:
          type: object
          properties:
            logradouro:
              type: string
              minLength: 1
            numero:
              type: string
              minLength: 1
            complemento:
              type: string
            bairro:
              type: string
              minLength: 1
            municipio:
              type: string
              minLength: 1
            uf:
              type: string
              minLength: 2
              maxLength: 2
            cep:
              type: string
              minLength: 8
              maxLength: 9
            codigoMunicipio:
              type: string
          required:
            - logradouro
            - numero
            - bairro
            - municipio
            - uf
            - cep
        inscricao_estadual:
          type: string
          maxLength: 50
        email:
          anyOf:
            - type: string
              format: email
            - type: string
              enum:
                - ""
        telefone:
          type: string
          maxLength: 20
      description: Payload parcial de atualização de Cliente (todos os campos opcionais).
    NotaApiV1:
      type: object
      properties:
        id:
          type: string
        status:
          type: string
        numero:
          type:
            - string
            - "null"
        serie:
          type:
            - string
            - "null"
        competencia:
          type:
            - string
            - "null"
        valorTotal:
          type:
            - number
            - "null"
        xmlUrl:
          type:
            - string
            - "null"
          format: uri
        pdfUrl:
          type:
            - string
            - "null"
          format: uri
        clienteId:
          type:
            - string
            - "null"
        servicoDescricao:
          type:
            - string
            - "null"
        emitidaEm:
          type:
            - string
            - "null"
          format: date-time
        createdAt:
          type:
            - string
            - "null"
          format: date-time
        updatedAt:
          type:
            - string
            - "null"
          format: date-time
      required:
        - id
        - status
        - numero
        - serie
        - competencia
        - valorTotal
        - xmlUrl
        - pdfUrl
        - clienteId
        - servicoDescricao
        - emitidaEm
        - createdAt
        - updatedAt
      description: DTO público da NFS-e (contrato v1 estável, independente do engine).
    ApiV1ErrorBody:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: "Código do erro em snake_case. Valores: invalid_input, unauthenticated, forbidden, not_found, rate_limit_exceeded, upstream_error, internal."
              example: invalid_input
            message:
              type: string
              description: Mensagem legível para humanos (pode ser traduzida).
            details:
              description: "Detalhes adicionais (ex.: campos Zod inválidos). Formato depende do `code`."
          required:
            - code
            - message
      required:
        - error
      description: Envelope canônico de erro para respostas `/api/v1/*`. Story 5.4.
    Pagination:
      type: object
      properties:
        page:
          type: integer
          minimum: 1
          example: 1
        perPage:
          type: integer
          minimum: 1
          maximum: 100
          example: 25
        total:
          type: integer
          minimum: 0
          example: 42
      required:
        - page
        - perPage
        - total
    Cliente:
      type: object
      properties:
        id:
          type: string
        cnpj:
          type:
            - string
            - "null"
        cpf:
          type:
            - string
            - "null"
        razao_social:
          type: string
        nome_fantasia:
          type:
            - string
            - "null"
        inscricao_estadual:
          type:
            - string
            - "null"
        email:
          type:
            - string
            - "null"
          format: email
        telefone:
          type:
            - string
            - "null"
        endereco:
          type: object
          properties:
            logradouro:
              type: string
            numero:
              type: string
            complemento:
              type: string
            bairro:
              type: string
            municipio:
              type: string
            uf:
              type: string
              minLength: 2
              maxLength: 2
            cep:
              type: string
            codigoMunicipio:
              type: string
          required:
            - logradouro
            - numero
            - bairro
            - municipio
            - uf
            - cep
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      required:
        - id
        - cnpj
        - cpf
        - razao_social
        - nome_fantasia
        - inscricao_estadual
        - email
        - telefone
        - endereco
        - created_at
        - updated_at
      description: Cliente cadastrado no tenant (tomador de serviço para emissão de NFS-e).
    ContatoFinanceiro:
      type: object
      properties:
        id:
          type: string
        tipo:
          type: string
          enum:
            - email
            - whatsapp
        valor:
          type: string
        principal:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - tipo
        - valor
        - principal
        - createdAt
        - updatedAt
      description: Canal de distribuição financeira (email ou WhatsApp) associado ao Cliente. Um por cliente pode ser `principal`.
    ContatoFinanceiroCreate:
      type: object
      properties:
        tipo:
          type: string
          enum:
            - email
            - whatsapp
        valor:
          type: string
          minLength: 1
          maxLength: 255
        principal:
          type: boolean
          default: false
      required:
        - tipo
        - valor
      description: Payload de criação de contato_financeiro. `valor` valida por tipo — email RFC-like para `tipo=email`, E.164 sem `+` para `tipo=whatsapp`.
    ContatoFinanceiroUpdate:
      type: object
      properties:
        valor:
          type: string
          minLength: 1
          maxLength: 255
        principal:
          type: boolean
      description: Payload parcial de atualização (valor OU principal). Ao menos um campo obrigatório.
    Cobranca:
      type: object
      properties:
        id:
          type: string
        status:
          type: string
          enum:
            - pendente
            - consumida
            - cancelada
            - expirada
        valor:
          type: number
        tomadorCnpj:
          type:
            - string
            - "null"
        tomadorRazaoSocial:
          type:
            - string
            - "null"
        servicoDescricao:
          type: string
        codigoServico:
          type:
            - string
            - "null"
        aliquotaIss:
          type:
            - number
            - "null"
        usosMaximos:
          type:
            - integer
            - "null"
        usosAtual:
          type: integer
        expiresAt:
          type:
            - string
            - "null"
          format: date-time
        formId:
          type:
            - string
            - "null"
        formSlug:
          type:
            - string
            - "null"
        url:
          type:
            - string
            - "null"
          format: uri
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - status
        - valor
        - tomadorCnpj
        - tomadorRazaoSocial
        - servicoDescricao
        - codigoServico
        - aliquotaIss
        - usosMaximos
        - usosAtual
        - expiresAt
        - formId
        - formSlug
        - url
        - createdAt
        - updatedAt
      description: "Cobrança: link pré-preenchido de formulário público para o tomador emitir a nota. Cria NFS-e ao ser consumida (uso pessoal ou reuse até `usosMaximos`)."
    CobrancaCreate:
      type: object
      properties:
        formId:
          type:
            - string
            - "null"
          minLength: 1
        valor:
          type: number
          exclusiveMinimum: 0
          minimum: 0.01
        descricao:
          type:
            - string
            - "null"
          maxLength: 500
        servicoDescricao:
          type: string
          minLength: 1
          maxLength: 500
        codigoServico:
          type: string
          pattern: ^[0-9]{4,10}$
        aliquotaIss:
          type:
            - number
            - "null"
          minimum: 0
          maximum: 100
        usosMaximos:
          type:
            - integer
            - "null"
          exclusiveMinimum: 0
        expiresAt:
          type: string
        clienteId:
          type:
            - string
            - "null"
      required:
        - valor
        - servicoDescricao
        - codigoServico
      description: "Payload de criação de cobrança. Requer `formId` (form público existente), valor, `servicoDescricao`. Opcional: `codigoServico`, `aliquotaIss`, `usosMaximos`, `expiresAt`."
    CobrancaUpdate:
      type: object
      properties:
        valor:
          type: number
          exclusiveMinimum: 0
          minimum: 0.01
        descricao:
          type:
            - string
            - "null"
          maxLength: 500
        servicoDescricao:
          type: string
          minLength: 1
          maxLength: 500
        codigoServico:
          type: string
          pattern: ^[0-9]{4,10}$
        aliquotaIss:
          type:
            - number
            - "null"
          minimum: 0
          maximum: 100
        usosMaximos:
          type:
            - integer
            - "null"
          exclusiveMinimum: 0
        expiresAt:
          type: string
        clienteId:
          type:
            - string
            - "null"
      description: Payload parcial de atualização de cobrança (só permitida em status `pendente`).
    CobrancaCancel:
      type: object
      properties:
        reason:
          type:
            - string
            - "null"
          maxLength: 500
      description: Motivo (opcional) do cancelamento da cobrança.
    ConfiguracaoNfse:
      type: object
      properties:
        userId:
          type: string
        ambiente:
          type: string
          enum:
            - homologacao
            - producao
        regimeTributario:
          type:
            - string
            - "null"
        dadosPrestador:
          type:
            - object
            - "null"
          additionalProperties: {}
        emailConfig:
          type:
            - object
            - "null"
          additionalProperties: {}
        hasCertificate:
          type: boolean
        hasWhatsappInstance:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - userId
        - ambiente
        - regimeTributario
        - dadosPrestador
        - emailConfig
        - hasCertificate
        - hasWhatsappInstance
        - createdAt
        - updatedAt
      description: Configuração NFS-e do prestador. Campos sensíveis (certificado A1 PFX, whatsapp userToken) NÃO são expostos — apenas flags booleanos.
    ConfiguracaoNfseUpsert:
      type: object
      properties:
        cnpj_prestador:
          type: string
          minLength: 14
          maxLength: 18
        inscricao_municipal:
          type: string
          maxLength: 30
        regime_tributario:
          type: string
          enum:
            - simples_nacional
            - lucro_presumido
            - lucro_real
            - mei
        codigo_municipio:
          type: string
          minLength: 1
          maxLength: 10
        ambiente:
          type: string
          enum:
            - homologacao
            - producao
        codigo_servico_padrao:
          anyOf:
            - type: string
              minLength: 6
              maxLength: 6
              pattern: ^[0-9]{6}$
            - type: string
              enum:
                - ""
        item_lista_servico:
          anyOf:
            - type: string
              maxLength: 6
            - type: string
              enum:
                - ""
        codigo_cnae:
          anyOf:
            - type: string
              maxLength: 7
              pattern: ^[0-9]*$
            - type: string
              enum:
                - ""
        aliquota_iss_padrao:
          type:
            - number
            - "null"
          minimum: 0
          maximum: 100
        descricao_servico_padrao:
          anyOf:
            - type: string
              maxLength: 2000
            - type: string
              enum:
                - ""
        certificado_a1_base64:
          type: string
        certificado_a1_senha:
          type: string
        email_provider:
          type:
            - string
            - "null"
          enum:
            - smtp
            - ses
        email_config:
          anyOf:
            - type: object
              properties:
                host:
                  type: string
                  minLength: 1
                port:
                  type: integer
                  exclusiveMinimum: 0
                user:
                  type: string
                  minLength: 1
                pass:
                  type: string
                  minLength: 1
                from:
                  type: string
                  format: email
              required:
                - host
                - port
                - user
                - pass
                - from
            - type: object
              properties:
                region:
                  type: string
                  minLength: 1
                accessKeyId:
                  type: string
                  minLength: 1
                secretAccessKey:
                  type: string
                  minLength: 1
                from:
                  type: string
                  format: email
              required:
                - region
                - accessKeyId
                - secretAccessKey
                - from
            - type: "null"
            - type: "null"
        wuzapi_url:
          anyOf:
            - type: string
              format: uri
            - type: string
              enum:
                - ""
        wuzapi_token:
          type: string
      description: Payload de upsert da configuração. Certificado A1 é aceito como base64 (PFX) + senha; storage cifra em rest.
    FormPublico:
      type: object
      properties:
        id:
          type: string
        slug:
          type: string
        titulo:
          type: string
        descricao:
          type:
            - string
            - "null"
        config:
          type: object
          additionalProperties: {}
        servicoPadrao:
          type: object
          additionalProperties: {}
        modoValor:
          type:
            - string
            - "null"
          enum:
            - fixo
            - variavel
            - sugerido
        valorSugerido:
          type:
            - number
            - "null"
        termosCustom:
          type:
            - string
            - "null"
        notifyEmail:
          type: boolean
        notifyWhatsapp:
          type: boolean
        captchaEnabled:
          type: boolean
        redirectAfter:
          type:
            - string
            - "null"
        status:
          type: string
          enum:
            - ativo
            - pausado
            - arquivado
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        url:
          type:
            - string
            - "null"
          format: uri
      required:
        - id
        - slug
        - titulo
        - descricao
        - config
        - servicoPadrao
        - modoValor
        - valorSugerido
        - termosCustom
        - notifyEmail
        - notifyWhatsapp
        - captchaEnabled
        - redirectAfter
        - status
        - createdAt
        - updatedAt
        - url
      description: Form público reutilizável. URL pública `/f/{slug}` recebe submissions do tomador e cria NFS-e assíncrona.
    FormPublicoCreate:
      type: object
      properties:
        slug:
          type: string
          minLength: 3
          maxLength: 50
          description: Slug URL-friendly. Auto-suggest quando omitido.
        titulo:
          type: string
          minLength: 1
          maxLength: 200
        descricao:
          type:
            - string
            - "null"
        config:
          type: object
          additionalProperties: {}
          description: Estrutura completa do form (fields, settings). Validado por `formConfigSchema` no service.
        servicoPadrao:
          type: object
          additionalProperties: {}
          description: Template do serviço padrão (descrição, código, alíquota).
        modoValor:
          type: string
          enum:
            - fixo
            - variavel
            - sugerido
        valorSugerido:
          anyOf:
            - type: number
            - type: string
            - type: "null"
            - type: "null"
        termosCustom:
          type:
            - string
            - "null"
        notifyEmail:
          type: boolean
        notifyWhatsapp:
          type: boolean
        captchaEnabled:
          type: boolean
        redirectAfter:
          type:
            - string
            - "null"
        status:
          type: string
          enum:
            - ativo
            - pausado
            - arquivado
      required:
        - titulo
        - config
        - servicoPadrao
      description: Payload de criação de form público. `config` e `servicoPadrao` são estruturados via schemas do domínio Forms (Epic 4).
    SubmitForm:
      type: object
      properties:
        id:
          type: string
        formId:
          type: string
        respostas:
          type: object
          additionalProperties: {}
        aceitouTermos:
          type: boolean
        ipAddress:
          type:
            - string
            - "null"
        userAgent:
          type:
            - string
            - "null"
        notaFiscalId:
          type:
            - string
            - "null"
        createdAt:
          type: string
          format: date-time
      required:
        - id
        - formId
        - respostas
        - aceitouTermos
        - ipAddress
        - userAgent
        - notaFiscalId
        - createdAt
      description: Submission de form público (dados do tomador + estado de emissão da nota).
    ReenviarEmailV1Body:
      type: object
      properties:
        email:
          type: string
          format: email
          description: Email override. Quando omitido, usa contato_financeiro `principal` do cliente (BR-7 fallback).
      description: Body opcional para reenvio de email da NFS-e.
    ReenviarWhatsappV1Body:
      type: object
      properties:
        telefone:
          type: string
          pattern: ^[1-9][0-9]{7,14}$
          description: "Telefone override em E.164 sem `+` (ex.: `5511959921219`). Quando omitido, usa contato principal (BR-7 fallback)."
      description: Body opcional para reenvio via WhatsApp da NFS-e.
    NotaPdfEnvelope:
      type: object
      properties:
        data:
          type: object
          properties:
            pdfUrl:
              type: string
              format: uri
              description: URL assinada (short-lived) para download do PDF. Regenera cada request.
            expiresAt:
              type: string
              format: date-time
          required:
            - pdfUrl
            - expiresAt
      required:
        - data
  parameters: {}
paths:
  /api/v1/notas/emitir:
    post:
      summary: Emitir NFS-e
      description: "Emite uma nota fiscal de serviço eletrônica. Wrapper síncrono: aguarda resposta da API Nacional. Dispara webhooks `nota.emitida` (assíncrono) e distribuição por email/WhatsApp (fire-and-forget)."
      tags:
        - Notas
      security:
        - bearerAuth:
            - notas.write
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DpsInput"
      responses:
        "201":
          description: Nota emitida (ou pendente aguardando API Nacional).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/NotaApiV1"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/notas:
    get:
      summary: Listar NFS-e
      description: Lista paginada de notas fiscais do tenant, com filtros por status, cliente, serviço e intervalo de emissão.
      tags:
        - Notas
      security:
        - bearerAuth:
            - notas.read
      parameters:
        - schema:
            type: string
            enum:
              - pendente
              - emitida
              - rejeitada
              - cancelada
          required: false
          name: status
          in: query
        - schema:
            type: string
            minLength: 1
          required: false
          name: clienteId
          in: query
        - schema:
            type: string
            minLength: 1
          required: false
          name: servicoId
          in: query
        - schema:
            type:
              - string
              - "null"
          required: false
          name: dataInicio
          in: query
        - schema:
            type:
              - string
              - "null"
          required: false
          name: dataFim
          in: query
        - schema:
            type: integer
            minimum: 1
            default: 1
          required: false
          name: page
          in: query
        - schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
          required: false
          name: perPage
          in: query
        - schema:
            type: string
            enum:
              - data_emissao
              - valor
              - status
              - created_at
          required: false
          name: sortBy
          in: query
        - schema:
            type: string
            enum:
              - asc
              - desc
            default: desc
          required: false
          name: sortOrder
          in: query
      responses:
        "200":
          description: Lista paginada.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      items:
                        type: array
                        items:
                          $ref: "#/components/schemas/NotaApiV1"
                      pagination:
                        $ref: "#/components/schemas/Pagination"
                    required:
                      - items
                      - pagination
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/notas/{id}:
    get:
      summary: Detalhe de NFS-e
      description: Retorna os detalhes de uma NFS-e do tenant.
      tags:
        - Notas
      security:
        - bearerAuth:
            - notas.read
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Detalhe da nota.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/NotaApiV1"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/notas/{id}/cancelar:
    post:
      summary: Cancelar NFS-e
      description: Solicita cancelamento junto à API Nacional. Emite webhook `nota.cancelada` em sucesso; retorna `upstream_error` se rejeitado.
      tags:
        - Notas
      security:
        - bearerAuth:
            - notas.write
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CancelarNfseInput"
      responses:
        "200":
          description: Cancelamento aceito pela API Nacional.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/NotaApiV1"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/notas/{id}/reemitir:
    post:
      summary: Reemitir NFS-e
      description: Reemite uma nota fiscal a partir de outra (rejeitada ou cancelada). Aceita override parcial do payload DPS original.
      tags:
        - Notas
      security:
        - bearerAuth:
            - notas.write
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          name: id
          in: path
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                cliente_id:
                  type: string
                  format: uuid
                servico_id:
                  type: string
                  format: uuid
                parcela_id:
                  type: string
                  format: uuid
                servico_recorrente_id:
                  type: string
                  format: uuid
                tomador_cnpj:
                  type: string
                  minLength: 14
                  maxLength: 18
                tomador_cpf:
                  type: string
                  minLength: 11
                  maxLength: 14
                tomador_nome:
                  type: string
                  minLength: 1
                tomador_razao_social:
                  type: string
                  minLength: 1
                tomador_logradouro:
                  type: string
                tomador_numero:
                  type: string
                tomador_bairro:
                  type: string
                tomador_municipio:
                  type: string
                tomador_uf:
                  type: string
                  minLength: 2
                  maxLength: 2
                tomador_cep:
                  type: string
                tomador_email:
                  anyOf:
                    - type: string
                      format: email
                    - type: string
                      enum:
                        - ""
                tomador_telefone:
                  type: string
                valor:
                  type: number
                  exclusiveMinimum: 0
                descricao_servico:
                  type: string
                  minLength: 1
                codigo_servico:
                  type: string
                aliquota_iss:
                  type: number
                  minimum: 0
                  maximum: 100
              description: Override parcial do DPS original. Omita para reemitir com o payload da nota origem.
      responses:
        "201":
          description: Nota reemitida.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                      - $ref: "#/components/schemas/NotaApiV1"
                      - type: object
                        properties:
                          originalId:
                            type: string
                            description: ID da nota origem (rejeitada/cancelada).
                        required:
                          - originalId
                    description: DTO público da NFS-e (contrato v1 estável, independente do engine).
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/clientes:
    post:
      summary: Criar Cliente
      description: Cadastra um cliente (tomador de NFS-e). CNPJ único por tenant — duplicata retorna 409 conflict (mapeado como `invalid_input`).
      tags:
        - Clientes
      security:
        - bearerAuth:
            - clientes.write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                cnpj:
                  type: string
                  minLength: 14
                  maxLength: 18
                cpf:
                  type: string
                  minLength: 11
                  maxLength: 14
                razao_social:
                  type: string
                  minLength: 1
                  maxLength: 255
                nome_fantasia:
                  type: string
                  maxLength: 255
                endereco:
                  type: object
                  properties:
                    logradouro:
                      type: string
                      minLength: 1
                    numero:
                      type: string
                      minLength: 1
                    complemento:
                      type: string
                    bairro:
                      type: string
                      minLength: 1
                    municipio:
                      type: string
                      minLength: 1
                    uf:
                      type: string
                      minLength: 2
                      maxLength: 2
                    cep:
                      type: string
                      minLength: 8
                      maxLength: 9
                    codigoMunicipio:
                      type: string
                  required:
                    - logradouro
                    - numero
                    - bairro
                    - municipio
                    - uf
                    - cep
                inscricao_estadual:
                  type: string
                  maxLength: 50
                email:
                  anyOf:
                    - type: string
                      format: email
                    - type: string
                      enum:
                        - ""
                telefone:
                  type: string
                  maxLength: 20
              required:
                - razao_social
                - endereco
      responses:
        "201":
          description: Cliente criado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Cliente"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
    get:
      summary: Listar Clientes
      description: Lista paginada de clientes do tenant. Search por razão social ou CNPJ.
      tags:
        - Clientes
      security:
        - bearerAuth:
            - clientes.read
      parameters:
        - schema:
            type: integer
            exclusiveMinimum: 0
            default: 1
          required: false
          name: page
          in: query
        - schema:
            type: integer
            exclusiveMinimum: 0
            maximum: 100
            default: 25
          required: false
          name: perPage
          in: query
        - schema:
            type: string
          required: false
          name: search
          in: query
      responses:
        "200":
          description: Lista paginada.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Cliente"
                  meta:
                    type: object
                    properties:
                      pagination:
                        $ref: "#/components/schemas/Pagination"
                    required:
                      - pagination
                required:
                  - data
                  - meta
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/clientes/{id}:
    patch:
      summary: Atualizar Cliente
      description: Atualiza campos de um cliente (partial). Retorna o cliente atualizado.
      tags:
        - Clientes
      security:
        - bearerAuth:
            - clientes.write
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                cnpj:
                  type: string
                  minLength: 14
                  maxLength: 18
                cpf:
                  type: string
                  minLength: 11
                  maxLength: 14
                razao_social:
                  type: string
                  minLength: 1
                  maxLength: 255
                nome_fantasia:
                  type: string
                  maxLength: 255
                endereco:
                  type: object
                  properties:
                    logradouro:
                      type: string
                      minLength: 1
                    numero:
                      type: string
                      minLength: 1
                    complemento:
                      type: string
                    bairro:
                      type: string
                      minLength: 1
                    municipio:
                      type: string
                      minLength: 1
                    uf:
                      type: string
                      minLength: 2
                      maxLength: 2
                    cep:
                      type: string
                      minLength: 8
                      maxLength: 9
                    codigoMunicipio:
                      type: string
                  required:
                    - logradouro
                    - numero
                    - bairro
                    - municipio
                    - uf
                    - cep
                inscricao_estadual:
                  type: string
                  maxLength: 50
                email:
                  anyOf:
                    - type: string
                      format: email
                    - type: string
                      enum:
                        - ""
                telefone:
                  type: string
                  maxLength: 20
      responses:
        "200":
          description: Cliente atualizado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Cliente"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
    get:
      summary: Detalhe de Cliente
      description: Retorna o cliente do tenant (shallow — sem contatos aninhados).
      tags:
        - Clientes
      security:
        - bearerAuth:
            - clientes.read
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Detalhe do cliente.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Cliente"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
    delete:
      summary: Excluir Cliente (soft delete)
      description: Marca o cliente como excluído (soft delete). Idempotente — reidempotência retorna 204 sem novo evento.
      tags:
        - Clientes
      security:
        - bearerAuth:
            - clientes.write
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          name: id
          in: path
      responses:
        "204":
          description: Cliente excluído (ou já estava excluído).
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/notas/{id}/consultar:
    post:
      summary: Consultar NFS-e na API Nacional
      description: Força consulta síncrona ao provider (API Nacional) e atualiza o status/XML local da nota (útil quando a nota está `pendente` aguardando processamento).
      tags:
        - Notas
      security:
        - bearerAuth:
            - notas.read
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Nota atualizada com resultado da consulta.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/NotaApiV1"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/notas/{id}/pdf:
    get:
      summary: URL assinada do PDF da NFS-e
      description: Retorna URL assinada (short-lived) para download do PDF. Não retorna o binário; use `pdfUrl` em client. Regenera cada request.
      tags:
        - Notas
      security:
        - bearerAuth:
            - notas.read
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          name: id
          in: path
      responses:
        "200":
          description: URL assinada + `expiresAt`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotaPdfEnvelope"
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/notas/{id}/reenviar-email:
    post:
      summary: Reenviar NFS-e por email
      description: Reenvia a NFS-e para o email do tomador. Body opcional — quando omitido usa o contato_financeiro `principal` do cliente (BR-7 fallback).
      tags:
        - Notas
      security:
        - bearerAuth:
            - notas.write
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          name: id
          in: path
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReenviarEmailV1Body"
      responses:
        "200":
          description: Reenvio agendado (fire-and-forget).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      ok:
                        type: boolean
                        enum:
                          - true
                      email:
                        type: string
                        format: email
                    required:
                      - ok
                      - email
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/notas/{id}/reenviar-whatsapp:
    post:
      summary: Reenviar NFS-e por WhatsApp
      description: Reenvia a NFS-e via WhatsApp (WuzAPI). Body opcional — quando omitido usa o contato principal.
      tags:
        - Notas
      security:
        - bearerAuth:
            - notas.write
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          name: id
          in: path
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReenviarWhatsappV1Body"
      responses:
        "200":
          description: Reenvio agendado (fire-and-forget).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      ok:
                        type: boolean
                        enum:
                          - true
                      telefone:
                        type: string
                    required:
                      - ok
                      - telefone
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/clientes/{id}/contatos:
    get:
      summary: Listar Contatos financeiros do Cliente
      description: Lista todos os contatos_financeiros (email/whatsapp) associados ao Cliente.
      tags:
        - Contatos
      security:
        - bearerAuth:
            - clientes.read
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Lista de contatos.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ContatoFinanceiro"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
    post:
      summary: Criar Contato financeiro
      description: Cria um contato_financeiro no Cliente. Marcar como `principal=true` remove `principal` de outros contatos do mesmo tipo (invariant single-principal).
      tags:
        - Contatos
      security:
        - bearerAuth:
            - clientes.write
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                tipo:
                  type: string
                  enum:
                    - email
                    - whatsapp
                valor:
                  type: string
                  minLength: 1
                  maxLength: 255
                principal:
                  type: boolean
                  default: false
              required:
                - tipo
                - valor
      responses:
        "201":
          description: Contato criado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ContatoFinanceiro"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/clientes/{id}/contatos/{contatoId}:
    patch:
      summary: Atualizar Contato financeiro
      description: Atualiza `valor` e/ou `principal` do contato. Preserva invariant single-principal por tipo.
      tags:
        - Contatos
      security:
        - bearerAuth:
            - clientes.write
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          description: ID do Cliente
          name: id
          in: path
        - schema:
            type: string
            example: b7d1f4c3-2e5a-4b8c-9d0e-1a2b3c4d5e6f
          required: true
          description: ID do contato_financeiro
          name: contatoId
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                valor:
                  type: string
                  minLength: 1
                  maxLength: 255
                principal:
                  type: boolean
      responses:
        "200":
          description: Contato atualizado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ContatoFinanceiro"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
    delete:
      summary: Excluir Contato financeiro
      description: Remove um contato_financeiro. Se era o `principal`, o próximo contato do mesmo tipo assume automaticamente.
      tags:
        - Contatos
      security:
        - bearerAuth:
            - clientes.write
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          description: ID do Cliente
          name: id
          in: path
        - schema:
            type: string
            example: b7d1f4c3-2e5a-4b8c-9d0e-1a2b3c4d5e6f
          required: true
          description: ID do contato_financeiro
          name: contatoId
          in: path
      responses:
        "204":
          description: Contato excluído.
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/cobrancas:
    post:
      summary: Criar Cobrança
      description: Cria uma cobrança (link de pagamento reutilizável). Requer form público existente (`formId`).
      tags:
        - Cobranças
      security:
        - bearerAuth:
            - cobrancas.write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                formId:
                  type:
                    - string
                    - "null"
                  minLength: 1
                valor:
                  type: number
                  exclusiveMinimum: 0
                  minimum: 0.01
                descricao:
                  type:
                    - string
                    - "null"
                  maxLength: 500
                servicoDescricao:
                  type: string
                  minLength: 1
                  maxLength: 500
                codigoServico:
                  type: string
                  pattern: ^[0-9]{4,10}$
                aliquotaIss:
                  type:
                    - number
                    - "null"
                  minimum: 0
                  maximum: 100
                usosMaximos:
                  type:
                    - integer
                    - "null"
                  exclusiveMinimum: 0
                expiresAt:
                  type: string
                clienteId:
                  type:
                    - string
                    - "null"
              required:
                - valor
                - servicoDescricao
                - codigoServico
      responses:
        "201":
          description: Cobrança criada.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Cobranca"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
    get:
      summary: Listar Cobranças
      description: Lista paginada de cobranças do tenant. Filtros por status, formId, período.
      tags:
        - Cobranças
      security:
        - bearerAuth:
            - cobrancas.read
      parameters:
        - schema:
            type: string
            enum:
              - pendente
              - emitindo
              - esgotada
              - expirada
              - cancelada
          required: false
          name: status
          in: query
        - schema:
            type: string
          required: false
          name: formId
          in: query
        - schema:
            type: string
          required: false
          name: clienteId
          in: query
        - schema:
            type: string
          required: false
          name: search
          in: query
        - schema:
            type: integer
            exclusiveMinimum: 0
            default: 1
          required: false
          name: page
          in: query
        - schema:
            type: integer
            exclusiveMinimum: 0
            maximum: 100
            default: 20
          required: false
          name: perPage
          in: query
        - schema:
            type:
              - boolean
              - "null"
            default: false
          required: false
          name: showFullHistory
          in: query
      responses:
        "200":
          description: Lista paginada.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Cobranca"
                  meta:
                    type: object
                    properties:
                      pagination:
                        $ref: "#/components/schemas/Pagination"
                    required:
                      - pagination
                required:
                  - data
                  - meta
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/cobrancas/{id}:
    get:
      summary: Detalhe de Cobrança
      description: Retorna a cobrança do tenant, com slug do form (`formSlug`).
      tags:
        - Cobranças
      security:
        - bearerAuth:
            - cobrancas.read
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Detalhe da cobrança.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Cobranca"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
    patch:
      summary: Atualizar Cobrança
      description: Atualização parcial (apenas em status `pendente` e sem consumos). Rejeita com 409 caso já consumida.
      tags:
        - Cobranças
      security:
        - bearerAuth:
            - cobrancas.write
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          name: id
          in: path
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                valor:
                  type: number
                  exclusiveMinimum: 0
                  minimum: 0.01
                descricao:
                  type:
                    - string
                    - "null"
                  maxLength: 500
                servicoDescricao:
                  type: string
                  minLength: 1
                  maxLength: 500
                codigoServico:
                  type: string
                  pattern: ^[0-9]{4,10}$
                aliquotaIss:
                  type:
                    - number
                    - "null"
                  minimum: 0
                  maximum: 100
                usosMaximos:
                  type:
                    - integer
                    - "null"
                  exclusiveMinimum: 0
                expiresAt:
                  type: string
                clienteId:
                  type:
                    - string
                    - "null"
      responses:
        "200":
          description: Cobrança atualizada.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Cobranca"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/cobrancas/{id}/cancelar:
    post:
      summary: Cancelar Cobrança
      description: Cancela a cobrança (transição para status `cancelada`). Link deixa de aceitar consumos.
      tags:
        - Cobranças
      security:
        - bearerAuth:
            - cobrancas.write
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          name: id
          in: path
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type:
                    - string
                    - "null"
                  maxLength: 500
      responses:
        "200":
          description: Cobrança cancelada.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/Cobranca"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/configuracoes/nfse:
    get:
      summary: Ler Configuração NFS-e
      description: Retorna a configuração NFS-e do prestador (regime, ambiente, dados prestador, email config). Certificado A1 e whatsapp userToken retornam apenas flags booleanos.
      tags:
        - Configuração
      security:
        - bearerAuth:
            - config.read
      responses:
        "200":
          description: Configuração do prestador.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ConfiguracaoNfse"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
    put:
      summary: Atualizar Configuração NFS-e
      description: Upsert da configuração NFS-e. Certificado A1 aceito como base64 (PFX) + senha; storage cifra em rest.
      tags:
        - Configuração
      security:
        - bearerAuth:
            - config.write
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                cnpj_prestador:
                  type: string
                  minLength: 14
                  maxLength: 18
                inscricao_municipal:
                  type: string
                  maxLength: 30
                regime_tributario:
                  type: string
                  enum:
                    - simples_nacional
                    - lucro_presumido
                    - lucro_real
                    - mei
                codigo_municipio:
                  type: string
                  minLength: 1
                  maxLength: 10
                ambiente:
                  type: string
                  enum:
                    - homologacao
                    - producao
                codigo_servico_padrao:
                  anyOf:
                    - type: string
                      minLength: 6
                      maxLength: 6
                      pattern: ^[0-9]{6}$
                    - type: string
                      enum:
                        - ""
                item_lista_servico:
                  anyOf:
                    - type: string
                      maxLength: 6
                    - type: string
                      enum:
                        - ""
                codigo_cnae:
                  anyOf:
                    - type: string
                      maxLength: 7
                      pattern: ^[0-9]*$
                    - type: string
                      enum:
                        - ""
                aliquota_iss_padrao:
                  type:
                    - number
                    - "null"
                  minimum: 0
                  maximum: 100
                descricao_servico_padrao:
                  anyOf:
                    - type: string
                      maxLength: 2000
                    - type: string
                      enum:
                        - ""
                certificado_a1_base64:
                  type: string
                certificado_a1_senha:
                  type: string
                email_provider:
                  type:
                    - string
                    - "null"
                  enum:
                    - smtp
                    - ses
                email_config:
                  anyOf:
                    - type: object
                      properties:
                        host:
                          type: string
                          minLength: 1
                        port:
                          type: integer
                          exclusiveMinimum: 0
                        user:
                          type: string
                          minLength: 1
                        pass:
                          type: string
                          minLength: 1
                        from:
                          type: string
                          format: email
                      required:
                        - host
                        - port
                        - user
                        - pass
                        - from
                    - type: object
                      properties:
                        region:
                          type: string
                          minLength: 1
                        accessKeyId:
                          type: string
                          minLength: 1
                        secretAccessKey:
                          type: string
                          minLength: 1
                        from:
                          type: string
                          format: email
                      required:
                        - region
                        - accessKeyId
                        - secretAccessKey
                        - from
                    - type: "null"
                    - type: "null"
                wuzapi_url:
                  anyOf:
                    - type: string
                      format: uri
                    - type: string
                      enum:
                        - ""
                wuzapi_token:
                  type: string
      responses:
        "200":
          description: Configuração atualizada.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/ConfiguracaoNfse"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/forms:
    get:
      summary: Listar Forms públicos
      description: Lista paginada de forms públicos do prestador. Filtro por status.
      tags:
        - Forms
      security:
        - bearerAuth:
            - forms.read
      parameters:
        - schema:
            type: integer
            minimum: 1
            default: 1
          required: false
          name: page
          in: query
        - schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          required: false
          name: perPage
          in: query
        - schema:
            type: string
            enum:
              - ativo
              - pausado
              - arquivado
          required: false
          name: status
          in: query
      responses:
        "200":
          description: Lista paginada de forms.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/FormPublico"
                  meta:
                    type: object
                    properties:
                      page:
                        type: integer
                      perPage:
                        type: integer
                      total:
                        type: integer
                    required:
                      - page
                      - perPage
                      - total
                required:
                  - data
                  - meta
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
    post:
      summary: Criar Form público
      description: Cria um form público reutilizável. `slug` é auto-suggested quando omitido. Uniqueness de slug garantida por tenant.
      tags:
        - Forms
      security:
        - bearerAuth:
            - forms.write
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FormPublicoCreate"
      responses:
        "201":
          description: Form criado.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/FormPublico"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/forms/{id}:
    get:
      summary: Detalhe de Form público
      description: Retorna o form completo (config + settings + termos).
      tags:
        - Forms
      security:
        - bearerAuth:
            - forms.read
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          name: id
          in: path
      responses:
        "200":
          description: Detalhe do form.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: "#/components/schemas/FormPublico"
                required:
                  - data
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
  /api/v1/forms/{id}/submissions:
    get:
      summary: Listar Submissions do Form
      description: Lista paginada de submissions do form (respostas do tomador + link para NFS-e criada).
      tags:
        - Forms
      security:
        - bearerAuth:
            - forms.read
      parameters:
        - schema:
            type: string
            example: a3f8c2e1-4b6d-4c7f-9e0a-1b2c3d4e5f6a
          required: true
          name: id
          in: path
        - schema:
            type: integer
            minimum: 1
            default: 1
          required: false
          name: page
          in: query
        - schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          required: false
          name: perPage
          in: query
      responses:
        "200":
          description: Lista paginada de submissions.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/SubmitForm"
                  meta:
                    type: object
                    properties:
                      page:
                        type: integer
                      perPage:
                        type: integer
                      total:
                        type: integer
                    required:
                      - page
                      - perPage
                      - total
                required:
                  - data
                  - meta
        "400":
          description: Payload inválido (Zod validation, JSON malformado, etc.)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "401":
          description: Token ausente ou inválido (`unauthenticated`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "403":
          description: Token válido mas sem escopo requerido (`forbidden`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "404":
          description: Recurso não encontrado (`not_found`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "429":
          description: Rate limit excedido (`rate_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "500":
          description: Erro interno (`internal` — mensagem opaca).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
        "502":
          description: Provider externo (API Nacional NFS-e, email, WhatsApp) falhou (`upstream_error`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiV1ErrorBody"
webhooks: {}
