openapi: 3.1.0
info:
  title: EasyNotas — API de Emissão
  version: "1.1.0"
  description: |
    API REST para emitir, consultar e cancelar notas fiscais, inutilizar
    numeração de NF-e e ler as notas recebidas pelas empresas do escritório.

    **Documentos:** NFS-e (Ambiente Nacional ou emissor municipal de SP,
    roteado pelo município da empresa), NF-e modelo 55 (inclusive devolução)
    e os documentos de transporte CT-e, CT-e OS, CT-e Simplificado, MDF-e,
    NFCom e DC-e (disponíveis, ainda sem emissão real pela API).

    ### Modelo assíncrono
    `POST /v1/invoices` responde **202** com `status: "queued"`. A fila de
    emissão roda a cada 10 minutos. Acompanhe por webhook (recomendado) ou
    por `GET /v1/invoices/{id}`.

    ### Ambientes
    O header `X-EasyNotas-Environment` é obrigatório e precisa bater com o
    ambiente da API key. `sandbox` emite em homologação, sem valor fiscal e
    sem consumir o plano.

    ### Novidades da 1.1.0 (30/09/2026)
    - NFS-e: informações complementares, município da prestação, IBS/CBS por
      nota, ISS retido, retenções federais e IRRF automático para tomador PJ,
      tomador não identificado.
    - NF-e: série própria da empresa por padrão, item referenciado na
      devolução, cBenef e PIS/COFINS por item; `chave_acesso` na consulta.
    - Novas rotas: inutilização de numeração e notas recebidas/manifestação.
    - Novos escopos `numbering:write`, `received:read`, `received:manifest`,
      concedidos também às keys ativas já existentes.
  contact:
    name: Suporte EasyNotas
  license:
    name: Proprietária
    identifier: LicenseRef-EasyNotas-Proprietary

servers:
  - url: https://pufsgcaxubzxdtmsenjx.supabase.co/functions/v1/api-v1
    description: Produção e sandbox (o ambiente é escolhido pelo header)

security:
  - ApiKeyAuth: []

tags:
  - name: Notas
    description: Emissão, consulta e cancelamento
  - name: Numeração
    description: Inutilização de numeração de NF-e
  - name: Recebidas
    description: Notas emitidas contra as empresas do escritório (Buscador XML)
  - name: Sistema
    description: Verificação de disponibilidade

paths:
  /v1/health:
    get:
      operationId: health
      tags: [Sistema]
      summary: Verifica se a API está no ar
      security: []
      responses:
        "200":
          description: API disponível
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }

  /v1/invoices:
    post:
      operationId: createInvoice
      tags: [Notas]
      summary: Emitir nota fiscal
      description: |
        Aceita a solicitação e devolve **202**. A emissão é assíncrona.
        Escopo `invoices:write`.

        `Idempotency-Key` é **obrigatório**: reenviar a mesma chave com o mesmo
        corpo devolve a mesma resposta (header `Idempotency-Replayed: true`),
        em vez de emitir duas vezes. O `409 idempotency_conflict` só ocorre
        quando a chave já gerou um 202; após um 4xx/5xx a mesma chave pode ser
        reenviada com o corpo corrigido. A proteção vale por 30 dias.
      parameters:
        - $ref: "#/components/parameters/Environment"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/NfseNacionalInput"
                - $ref: "#/components/schemas/NfeInput"
                - $ref: "#/components/schemas/TransporteInput"
            examples:
              nfse_nacional:
                summary: NFS-e com Reforma Tributária e informações complementares
                value:
                  tipo: nfse_nacional
                  cnpj_emitente: "12345678000190"
                  tomador:
                    documento: "11222333000181"
                    razao_social: "Cliente Exemplo LTDA"
                    email: "financeiro@exemplo.com.br"
                  servico:
                    discriminacao: "Intermediação na venda do veículo Onix 2022"
                    valor_servicos: 1500.00
                    codigo_tributacao_nacional: "100501"
                    aliquota: 2.0
                    informacoes_complementares: "Veículo placa ABC1D23"
                    codigo_municipio_prestacao: "3502804"
                    codigo_nbs: "102010000"
              nfse_consumidor_final:
                summary: NFS-e sem tomador identificado
                value:
                  tipo: nfse_nacional
                  cnpj_emitente: "12345678000190"
                  servico:
                    discriminacao: "Diária de hospedagem - quarto standard"
                    valor_servicos: 180.00
                    codigo_tributacao_nacional: "090101"
              nfse_com_retencao:
                summary: NFS-e de emitente do Lucro Presumido com retenções
                value:
                  tipo: nfse_nacional
                  cnpj_emitente: "12345678000190"
                  tomador:
                    documento: "11222333000181"
                    razao_social: "Cliente Exemplo LTDA"
                  servico:
                    discriminacao: "Consultoria em gestão empresarial - setembro/2026"
                    valor_servicos: 10000.00
                    codigo_tributacao_nacional: "170101"
                    iss_retido: true
                    retencoes:
                      valor_ir: 150.00
                      valor_pis: 65.00
                      valor_cofins: 300.00
                      valor_csll: 100.00
              nfe:
                summary: NF-e modelo 55 (venda)
                value:
                  tipo: nfe
                  cnpj_emitente: "12345678000190"
                  destinatario:
                    nome: "Consumidor Final"
                    documento: "11144477735"
                    contribuinte: false
                    endereco:
                      logradouro: "Rua A"
                      numero: "10"
                      bairro: "Centro"
                      municipio: "Araçatuba"
                      uf: "SP"
                      cep: "16010000"
                  itens:
                    - codigo: "PROD-001"
                      descricao: "Produto de teste"
                      quantidade: 2
                      valor_unitario: 50.0
                      ncm: "84713012"
                      unidade: "UN"
                      cst_csosn: "102"
                      origem: 0
              nfe_devolucao:
                summary: NF-e de devolução com item referenciado
                value:
                  tipo: nfe
                  cnpj_emitente: "12345678000190"
                  destinatario:
                    nome: "Supermercado Exemplo Ltda"
                    documento: "11444777000161"
                    ie: "111222333444"
                    contribuinte: true
                    endereco:
                      logradouro: "Av. Brasil"
                      numero: "1000"
                      bairro: "Centro"
                      municipio: "Araçatuba"
                      uf: "SP"
                      cep: "16010000"
                  itens:
                    - codigo: "FERM-01"
                      descricao: "Fermento biológico"
                      quantidade: 10
                      valor_unitario: 9.98
                      ncm: "21023000"
                      cst_csosn: "900"
                      aliquota_icms: 18
                      origem: 3
                      cfop: "5202"
                      numero_item_referenciado: 2
                  operacao:
                    natureza_operacao: "Devolução de compra"
                    finalidade_emissao: 4
                    chave_nfe_referenciada: "35260911444777000161550010000123451123456780"
      responses:
        "202":
          description: Solicitação aceita e enfileirada
          headers:
            Location:
              schema: { type: string, example: /v1/invoices/920d1d7a-f721-40d7-a44b-f79af97dd208 }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InvoiceAccepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409":
          description: "`idempotency_conflict` (mesma key, corpo diferente) ou `idempotency_in_flight`"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "422": { $ref: "#/components/responses/UnprocessableEntity" }
        "424": { $ref: "#/components/responses/FailedDependency" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

    get:
      operationId: listInvoices
      tags: [Notas]
      summary: Listar notas
      description: Notas do escritório, das mais novas para as mais antigas. Escopo `invoices:read`.
      parameters:
        - $ref: "#/components/parameters/Environment"
        - name: status
          in: query
          schema: { $ref: "#/components/schemas/FiscalStatus" }
        - name: tipo
          in: query
          schema: { $ref: "#/components/schemas/TipoNota" }
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PageSize"
      responses:
        "200":
          description: Página de notas
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/InvoiceSummary" }
                  page: { type: integer }
                  page_size: { type: integer }
                  has_more: { type: boolean }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/invoices/{id}:
    get:
      operationId: getInvoice
      tags: [Notas]
      summary: Consultar nota
      description: Escopo `invoices:read`. Enquanto `processing`, a resposta traz `Retry-After`.
      parameters:
        - $ref: "#/components/parameters/Environment"
        - $ref: "#/components/parameters/InvoiceId"
      responses:
        "200":
          description: Estado atual da nota
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Invoice" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/invoices/{id}/cancel:
    post:
      operationId: cancelInvoice
      tags: [Notas]
      summary: Cancelar nota
      description: |
        Responde **202**: o cancelamento é encaminhado e a confirmação chega
        pelo webhook `invoice.cancelled` ou pelo GET, normalmente em segundos.
        Até lá o GET continua `authorized`. Escopo `invoices:cancel` (keys
        geradas antes de 20/07/2026 não o têm).
      parameters:
        - $ref: "#/components/parameters/Environment"
        - $ref: "#/components/parameters/InvoiceId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [justificativa]
              properties:
                justificativa:
                  type: string
                  minLength: 15
                  maxLength: 255
                  example: "Serviço não executado conforme contrato"
      responses:
        "200":
          description: Nota já estava cancelada (idempotente)
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  status: { type: string, example: cancelled }
                  already_cancelled: { type: boolean, example: true }
        "202":
          description: Cancelamento encaminhado
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  status: { type: string, example: processing }
                  message: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: "`not_cancelable` — a nota não está autorizada"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "422": { $ref: "#/components/responses/UnprocessableEntity" }
        "502":
          description: "`cancel_dispatch_failed` — falha ao encaminhar; tente de novo"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/invoices/{id}/cancellation-window:
    get:
      operationId: getCancellationWindow
      tags: [Notas]
      summary: Consultar janela de cancelamento
      description: |
        Informa se a nota ainda está na janela **estimada** de cancelamento.
        Informativo. Leia `confiavel`: quando `false`, é estimativa. NFS-e
        devolve `prazo_horas: null` (regra municipal). Escopo `invoices:read`.
      parameters:
        - $ref: "#/components/parameters/Environment"
        - $ref: "#/components/parameters/InvoiceId"
      responses:
        "200":
          description: Janela estimada
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CancellationWindow" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/nfe/inutilizacoes:
    post:
      operationId: createInutilizacao
      tags: [Numeração]
      summary: Inutilizar faixa de numeração de NF-e
      description: |
        Evento **irreversível** na SEFAZ, síncrono: a resposta já traz o
        resultado. Em `sandbox` vai para a homologação e não entra no
        histórico (`id: null`). Faixa com número já transmitido pelo EasyNotas
        (inclusive cancelado) é recusada antes da SEFAZ. Escopo `numbering:write`.
        Sem `Idempotency-Key`: após um timeout, consulte o histórico (GET)
        antes de repetir.
      parameters:
        - $ref: "#/components/parameters/Environment"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InutilizacaoInput" }
      responses:
        "201":
          description: Inutilização homologada pela SEFAZ
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InutilizacaoResultado" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422":
          description: |
            Payload inválido, `number_already_used`, ou recusa da SEFAZ (neste
            caso o corpo é `InutilizacaoResultado` com `status: rejected`).
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/Error"
                  - $ref: "#/components/schemas/InutilizacaoResultado"
        "424": { $ref: "#/components/responses/FailedDependency" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }
    get:
      operationId: listInutilizacoes
      tags: [Numeração]
      summary: Histórico de inutilizações da empresa
      description: "Só produção, feitas pelo painel (origem app) ou pela API (origem api). Escopo `invoices:read` ou `numbering:write`."
      parameters:
        - $ref: "#/components/parameters/Environment"
        - $ref: "#/components/parameters/CnpjEmitente"
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PageSize"
      responses:
        "200":
          description: Página do histórico
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/InutilizacaoHistorico" }
                  page: { type: integer }
                  page_size: { type: integer }
                  has_more: { type: boolean }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/received:
    get:
      operationId: listReceived
      tags: [Recebidas]
      summary: Listar notas recebidas
      description: |
        Notas que fornecedores emitiram contra a empresa, já sincronizadas
        pelo Buscador XML do EasyNotas (a API não dispara sincronização).
        Escopo `received:read`.
      parameters:
        - $ref: "#/components/parameters/Environment"
        - $ref: "#/components/parameters/CnpjEmitente"
        - name: tipo
          in: query
          schema: { type: string, enum: [nfe, nfce, cte, nfse] }
          description: Sem filtro, lista todos os tipos exceto eventos.
        - name: desde
          in: query
          schema: { type: string, format: date }
          description: Data de emissão inicial (AAAA-MM-DD, fuso de São Paulo).
        - name: ate
          in: query
          schema: { type: string, format: date }
        - name: manifestacao
          in: query
          schema:
            type: string
            enum: [pendente, ciencia, confirmacao, desconhecimento, nao_realizada]
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PageSize"
      responses:
        "200":
          description: Página de notas recebidas
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/NotaRecebida" }
                  page: { type: integer }
                  page_size: { type: integer }
                  has_more: { type: boolean }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "403": { $ref: "#/components/responses/Forbidden" }

  /v1/received/{chave}:
    get:
      operationId: getReceived
      tags: [Recebidas]
      summary: Consultar nota recebida
      description: |
        Busca pela chave em todas as empresas do escritório. Quando o XML
        completo existe, `xml_url` é um link assinado válido por 10 minutos.
        Escopo `received:read`.
      parameters:
        - $ref: "#/components/parameters/Environment"
        - $ref: "#/components/parameters/Chave"
      responses:
        "200":
          description: Nota recebida
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/NotaRecebida"
                  - type: object
                    properties:
                      cnpj_empresa: { type: string, description: CNPJ da empresa do escritório que recebeu a nota. }
                      xml_url: { type: [string, "null"] }
                      xml_url_expires_in: { type: [integer, "null"], example: 600 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /v1/received/{chave}/manifest:
    post:
      operationId: manifestReceived
      tags: [Recebidas]
      summary: Manifestação do destinatário
      description: |
        Evento **real e irreversível** na SEFAZ; bloqueado em sandbox.
        Mesmo processo do painel: registra o evento, baixa o XML quando
        liberado e consome franquia do plano quando o XML é baixado.
        NF-e aceita `ciencia`, `confirmacao`, `desconhecimento`, `nao_realizada`;
        CT-e aceita só `desacordo`. Escopo `received:manifest`.
      parameters:
        - $ref: "#/components/parameters/Environment"
        - $ref: "#/components/parameters/Chave"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ManifestacaoInput" }
      responses:
        "200":
          description: Manifestação registrada
          content:
            application/json:
              schema:
                type: object
                properties:
                  chave: { type: string }
                  manifestacao: { type: string, example: ciencia }
                  xml_disponivel: { type: boolean }
                  consumiu_franquia: { type: boolean }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409":
          description: "`already_manifested`"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "422": { $ref: "#/components/responses/UnprocessableEntity" }
        "424": { $ref: "#/components/responses/FailedDependency" }

webhooks:
  invoiceStatusChanged:
    post:
      operationId: invoiceStatusChanged
      summary: Mudança de estado da nota
      description: |
        Enviado quando uma nota criada pela key é autorizada, rejeitada ou
        cancelada. `failed_internal` **não** gera webhook: consulte o GET para
        notas sem evento. Configure a URL em *Configurações → API /
        Desenvolvedores*; **salvar a URL gera um `whsec_` novo** e invalida o
        anterior.

        **Valide a assinatura**: `X-EasyNotas-Signature: t={unix},v1={hex}`,
        onde `v1 = HMAC-SHA256(secret, "{t}.{corpo cru}")` e `secret` é o
        `whsec_…` mostrado no cadastro. Recuse avisos com mais de 5 minutos.

        Entrega *at-least-once*: deduplique por `X-EasyNotas-Delivery`.
        Responda 2xx em até 10 s, sem redirecionar (3xx conta como falha).
        6 tentativas: a primeira na hora e as demais após 30 s, 2 min, 8 min,
        32 min e ~2 h. `User-Agent: EasyNotas-Webhooks/1`.
      parameters:
        - name: X-EasyNotas-Event
          in: header
          schema: { type: string, example: invoice.authorized }
        - name: X-EasyNotas-Delivery
          in: header
          schema: { type: string, format: uuid }
        - name: X-EasyNotas-Signature
          in: header
          schema: { type: string, example: "t=1790780400,v1=8a1f..." }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                event:
                  type: string
                  enum: [invoice.authorized, invoice.rejected, invoice.cancelled]
                created_at: { type: string, format: date-time }
                data:
                  type: object
                  properties:
                    id: { type: string, format: uuid }
                    status: { type: string, enum: [authorized, rejected, cancelled] }
                    tipo: { $ref: "#/components/schemas/TipoNota" }
                    environment: { type: string, enum: [sandbox, production] }
                    numero_nota: { type: [string, "null"] }
                    chave_nfe: { type: [string, "null"] }
                    pdf_url: { type: [string, "null"] }
                    xml_url: { type: [string, "null"] }
                    emitida_at: { type: [string, "null"], format: date-time }
                    cancelada_at: { type: [string, "null"], format: date-time }
      responses:
        "200":
          description: Recebido. Responda rápido e processe de forma assíncrona.

components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      description: |
        `Authorization: Bearer enk_live_...` (produção) ou `enk_test_...`
        (sandbox). Gere em *Configurações → API / Desenvolvedores*. O segredo
        é exibido **uma única vez**.

        Escopos: `invoices:write`, `invoices:read`, `invoices:cancel`,
        `numbering:write`, `received:read`, `received:manifest`. Os três
        últimos foram concedidos a todas as keys ativas em 30/09/2026.

  parameters:
    Environment:
      name: X-EasyNotas-Environment
      in: header
      required: true
      description: Precisa bater com o ambiente da API key.
      schema: { type: string, enum: [sandbox, production] }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: 16–64 caracteres em `[A-Za-z0-9_-]`.
      schema: { type: string, pattern: "^[A-Za-z0-9_-]{16,64}$" }
    InvoiceId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
    Chave:
      name: chave
      in: path
      required: true
      description: Chave de acesso — 44 dígitos (NF-e, NFC-e, CT-e) ou 50 (NFS-e Nacional).
      schema: { type: string, pattern: "^([0-9]{44}|[0-9]{50})$" }
    CnpjEmitente:
      name: cnpj_emitente
      in: query
      required: true
      description: CNPJ (14 dígitos) de uma empresa do seu escritório.
      schema: { type: string }
    Page:
      name: page
      in: query
      schema: { type: integer, minimum: 1, default: 1 }
    PageSize:
      name: page_size
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 20 }

  schemas:
    TipoNota:
      type: string
      enum: [nfse_nacional, nfe, cte, cte_os, cte_simp, mdfe, nfcom, dce]

    FiscalStatus:
      type: string
      enum: [queued, processing, authorized, rejected, cancelled, failed_internal]
      description: |
        `queued` na fila · `processing` em autorização · `authorized` autorizada
        · `rejected` recusada pelo fisco · `cancelled` cancelada ·
        `failed_internal` falha antes de chegar ao fisco.

    InvoiceAccepted:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string, example: queued }
        idempotency_key: { type: string }
        created_at: { type: string, format: date-time }

    InvoiceSummary:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { $ref: "#/components/schemas/FiscalStatus" }
        tipo: { $ref: "#/components/schemas/TipoNota" }
        numero: { type: [string, "null"] }
        pdf_url: { type: [string, "null"] }
        xml_url: { type: [string, "null"] }
        chave_acesso:
          type: [string, "null"]
          description: Chave de 44 dígitos (NF-e, CT-e, MDF-e…). NFS-e → null.
        created_at: { type: string, format: date-time }
        updated_at:
          type: string
          format: date-time
          description: Aproximação (data de emissão, de cancelamento ou de criação).

    Invoice:
      allOf:
        - $ref: "#/components/schemas/InvoiceSummary"
        - type: object
          properties:
            chave_nfe:
              type: [string, "null"]
              description: Mesmo valor de `chave_acesso`; mantido por compatibilidade.
            mensagem:
              type: [string, "null"]
              description: Motivo quando `rejected` ou `failed_internal`.

    CancellationWindow:
      type: object
      properties:
        id: { type: string, format: uuid }
        tipo: { type: string }
        pode_cancelar: { type: boolean }
        motivo: { type: string }
        prazo_horas:
          type: [integer, "null"]
          description: "`null` quando o prazo é desconhecido (NFS-e: regra municipal)."
        expira_em: { type: [string, "null"], format: date-time }
        segundos_restantes: { type: [integer, "null"] }
        confiavel:
          type: boolean
          description: "`false` = estimativa. Não trate como garantia."
        fonte: { type: string }

    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string }
            message: { type: string }
            field:
              type: string
              description: Caminho do campo com problema, quando aplicável.

    Endereco:
      type: object
      required: [logradouro, numero, bairro, municipio, uf, cep]
      properties:
        logradouro: { type: string, maxLength: 125 }
        numero: { type: string, maxLength: 60 }
        complemento: { type: string, maxLength: 60 }
        bairro: { type: string, maxLength: 60 }
        municipio: { type: string, maxLength: 60, description: Nome da cidade. }
        uf: { type: string, minLength: 2, maxLength: 2 }
        cep: { type: string, description: 8 dígitos. }

    EnderecoTomador:
      type: object
      description: |
        Opcional e tudo-ou-nada: se enviado, `logradouro`, `cep`, `municipio`
        e `uf` são obrigatórios.
      properties:
        logradouro: { type: string, maxLength: 125 }
        numero: { type: string, maxLength: 60 }
        complemento: { type: string, maxLength: 60 }
        bairro: { type: string, maxLength: 60 }
        municipio: { type: string, maxLength: 60 }
        uf: { type: string, minLength: 2, maxLength: 2 }
        cep: { type: string }

    NfseNacionalInput:
      type: object
      required: [tipo, cnpj_emitente, servico]
      properties:
        tipo: { type: string, enum: [nfse_nacional] }
        cnpj_emitente: { type: string, description: CNPJ da sua empresa emissora. }
        tomador:
          type: [object, "null"]
          description: |
            Omitido, `null` ou `{}` = tomador não identificado. Tomador parcial
            (ex.: nome sem documento) é recusado.
          properties:
            documento: { type: string, description: CPF (11) ou CNPJ (14). }
            razao_social: { type: string, maxLength: 150 }
            email: { type: string, format: email }
            codigo_municipio:
              type: string
              description: IBGE 7 dígitos. Validado, mas o município é resolvido pelo nome.
            endereco: { $ref: "#/components/schemas/EnderecoTomador" }
        servico:
          type: object
          required: [discriminacao, valor_servicos, codigo_tributacao_nacional]
          properties:
            discriminacao: { type: string, minLength: 15, maxLength: 2000 }
            valor_servicos: { type: number, exclusiveMinimum: 0 }
            codigo_tributacao_nacional: { type: string, description: 6 dígitos (aceita pontuado). }
            aliquota: { type: number, minimum: 0, description: Alíquota do ISS (%). }
            item_lista_servico:
              type: string
              description: Obrigatório para emitente de São Paulo capital.
            codigo_tributacao_municipio: { type: string }
            informacoes_complementares:
              type: string
              maxLength: 2000
              description: Nacional → xInfComp; SP → anexado à discriminação.
            codigo_municipio_prestacao:
              type: string
              description: IBGE (7 dígitos) do município da prestação.
            codigo_nbs: { type: string, description: 9 dígitos. Vence o padrão da empresa. }
            codigo_indicador_operacao: { type: string, description: 6 dígitos. Exige tomador com endereço completo. }
            ibs_cbs_situacao_tributaria: { type: string, description: 3 dígitos. }
            ibs_cbs_classificacao_tributaria: { type: string, description: 6 dígitos. }
            iss_retido: { type: boolean, default: false }
            retencoes:
              type: object
              description: |
                Só para emitente Lucro Presumido/Real (Simples/MEI com valor →
                422 `retencao_not_applicable`). Ausente → IRRF automático de
                1,5% para tomador PJ nos itens 10.xx da LC 116 (exceto 10.08)
                quando o IR passa de R$ 10 (nota a partir de R$ 667,00). `{}`
                ou zeros → sem retenção. No Ambiente Nacional PIS+COFINS+CSLL
                retidos vão somados num só campo, com CST PIS/COFINS 01 fixo.
              properties:
                valor_ir: { type: number, minimum: 0 }
                valor_pis: { type: number, minimum: 0 }
                valor_cofins: { type: number, minimum: 0 }
                valor_csll: { type: number, minimum: 0 }

    NfeInput:
      type: object
      required: [tipo, cnpj_emitente, destinatario, itens]
      properties:
        tipo: { type: string, enum: [nfe] }
        cnpj_emitente: { type: string }
        destinatario:
          type: object
          required: [nome, documento, endereco]
          properties:
            nome: { type: string, maxLength: 150 }
            documento: { type: string, description: CPF (11) ou CNPJ (14). }
            ie: { type: string, description: Inscrição estadual (obrigatória para contribuinte). }
            indicador_ie: { type: integer }
            contribuinte:
              type: boolean
              description: "`false` para consumidor final; `true` sem IE válida é rejeitado."
            email: { type: string, format: email }
            endereco: { $ref: "#/components/schemas/Endereco" }
        itens:
          type: array
          minItems: 1
          maxItems: 200
          items: { $ref: "#/components/schemas/NfeItem" }
        pagamento:
          type: array
          description: |
            A soma precisa bater com o total. Omitido → pagamento único 01
            (dinheiro) no valor total. Forma 90 sempre vai com valor 0. Na
            devolução é substituído por forma 90.
          items:
            type: object
            required: [forma, valor]
            properties:
              forma:
                type: string
                enum: ["01", "02", "03", "04", "05", "10", "11", "12", "13", "15", "16", "17", "18", "19", "90", "99"]
              valor: { type: number, minimum: 0 }
        operacao: { $ref: "#/components/schemas/NfeOperacao" }

    NfeOperacao:
      type: object
      properties:
        serie:
          type: [string, integer]
          description: Sem ela, vale a série exclusiva cadastrada na empresa (anti-rejeição 539).
        numero: { type: [string, integer], description: Numeração manual. }
        natureza_operacao: { type: string, default: "Venda de mercadoria" }
        finalidade_emissao:
          type: integer
          enum: [1, 2, 3, 4]
          default: 1
          description: 1 normal · 2 complementar · 3 ajuste · 4 devolução.
        chave_nfe_referenciada:
          type: string
          description: |
            44 dígitos. Obrigatória com finalidade 4 (referência por item,
            DFeReferenciado); nas demais vai no cabeçalho (refNFe).
        tipo_documento: { type: integer, enum: [0, 1], default: 1 }
        consumidor_final: { type: integer, enum: [0, 1] }
        presenca_comprador: { type: integer }
        modalidade_frete: { type: integer, default: 9 }
        valor_frete: { type: number }
        valor_seguro: { type: number }
        valor_outras_despesas: { type: number }
        valor_desconto: { type: number }
        informacoes_adicionais_contribuinte: { type: string, maxLength: 5000, description: "Texto maior é cortado." }
        informacoes_adicionais_fisco: { type: string, maxLength: 2000, description: "Texto maior é cortado." }
        data_emissao:
          type: string
          description: Gerada pelo servidor (horário de São Paulo). Não recomendado enviar.

    NfeItem:
      type: object
      required: [codigo, descricao, quantidade, valor_unitario, cst_csosn]
      properties:
        codigo: { type: string, maxLength: 60, description: Código do produto no seu sistema. }
        descricao: { type: string }
        quantidade: { type: number, exclusiveMinimum: 0 }
        valor_unitario: { type: number, minimum: 0 }
        ncm: { type: string, description: 8 dígitos; sem ele vale o padrão da empresa. }
        cest: { type: string }
        unidade: { type: string, example: UN }
        cfop:
          type: string
          description: "4 dígitos; omitido, derivado da operação. O 1º dígito é ajustado (5 ou 6) conforme a UF do destinatário."
        cst_csosn:
          type: string
          description: |
            Validado contra o regime da empresa (cadastro). Simples/MEI →
            CSOSN 101, 102, 103, 201, 202, 203, 300, 400, 500, 900.
            Regime normal → CST 00, 10, 20, 30, 40, 41, 50, 51, 60, 70, 90.
        origem: { type: integer, minimum: 0, maximum: 8, default: 0 }
        aliquota_icms: { type: number, minimum: 0, maximum: 100, description: "Omitida → alíquota interna da empresa; sem ela, 18%." }
        numero_item_referenciado:
          type: [integer, string]
          description: Devolução — nº do item (1–990) na NF-e original.
        codigo_beneficio_fiscal: { type: string, maxLength: 10, description: cBenef. }
        pis_cst: { type: string, description: 2 dígitos. }
        pis_aliquota: { type: number, minimum: 0, maximum: 100 }
        cofins_cst: { type: string, description: 2 dígitos. }
        cofins_aliquota: { type: number, minimum: 0, maximum: 100 }

    TransporteInput:
      type: object
      required: [tipo, cnpj_emitente, documento]
      description: |
        CT-e, CT-e OS, CT-e Simp, MDF-e, NFCom e DC-e. `documento` segue o
        formato da tela de emissão do app; o bloco `empresa` é sempre montado
        do cadastro. Exige add-on do tipo. Ainda sem emissão real pela API.
      properties:
        tipo: { type: string, enum: [cte, cte_os, cte_simp, mdfe, nfcom, dce] }
        cnpj_emitente: { type: string }
        documento: { type: object, additionalProperties: true }

    InutilizacaoInput:
      type: object
      required: [cnpj_emitente, serie, numero_inicial, numero_final, justificativa]
      properties:
        cnpj_emitente: { type: string }
        serie: { type: string, pattern: "^[0-9]{1,3}$", example: "1" }
        numero_inicial: { type: integer, minimum: 1 }
        numero_final: { type: integer, minimum: 1, description: No máximo 1000 números por chamada. }
        justificativa: { type: string, minLength: 15, maxLength: 255 }

    InutilizacaoResultado:
      type: object
      properties:
        id: { type: [string, "null"], format: uuid, description: null em sandbox. }
        environment: { type: string, enum: [sandbox, production] }
        status: { type: string, enum: [authorized, rejected] }
        serie: { type: string }
        numero_inicial: { type: integer }
        numero_final: { type: integer }
        protocolo_sefaz: { type: [string, "null"] }
        status_sefaz: { type: [string, "null"] }
        mensagem_sefaz: { type: [string, "null"] }

    InutilizacaoHistorico:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string, enum: [authorized, rejected] }
        serie: { type: string }
        numero_inicial: { type: integer }
        numero_final: { type: integer }
        justificativa: { type: string }
        protocolo_sefaz: { type: [string, "null"] }
        status_sefaz: { type: [string, "null"] }
        mensagem_sefaz: { type: [string, "null"] }
        origem: { type: string, enum: [app, api] }
        created_at: { type: string, format: date-time }

    NotaRecebida:
      type: object
      properties:
        chave: { type: string }
        tipo: { type: string, enum: [nfe, nfce, cte, nfse] }
        emitente:
          type: object
          properties:
            cnpj: { type: [string, "null"] }
            razao_social: { type: [string, "null"] }
        cnpj_destinatario: { type: [string, "null"] }
        valor: { type: [number, "null"] }
        data_emissao: { type: [string, "null"], format: date-time }
        situacao: { type: [string, "null"] }
        nivel:
          type: string
          enum: [resumo, completo]
          description: "`completo` só depois da manifestação (a SEFAZ libera o XML)."
        manifestacao:
          type: [string, "null"]
          enum: [ciencia, confirmacao, desconhecimento, nao_realizada, desacordo, null]
        xml_disponivel: { type: boolean }
        recebida_em: { type: string, format: date-time }

    ManifestacaoInput:
      type: object
      required: [cnpj_emitente, tipo]
      properties:
        cnpj_emitente:
          type: string
          description: CNPJ da SUA empresa (a destinatária da nota).
        tipo:
          type: string
          enum: [ciencia, confirmacao, desconhecimento, nao_realizada, desacordo]
        justificativa:
          type: string
          minLength: 15
          maxLength: 255
          description: Obrigatória para confirmacao, nao_realizada e desacordo.

  responses:
    BadRequest:
      description: Header ausente, JSON inválido ou filtro inválido
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: API key ausente, inválida ou expirada
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    PaymentRequired:
      description: Escritório bloqueado ou com pagamento pendente
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Forbidden:
      description: |
        Key revogada, ambiente divergente, sem escopo, empresa fora do
        escritório, tipo não habilitado, add-on ausente ou rota bloqueada em
        sandbox
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Recurso não encontrado neste escritório
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    UnprocessableEntity:
      description: Payload recusado na validação. `error.field` aponta o campo.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    FailedDependency:
      description: Empresa sem certificado ou sem configuração no emissor fiscal
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Limite de requisições (60/min por key, com X-RateLimit-Limit/Remaining) ou quota de 20 emissões/h por CNPJ (só emissões pela API)
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unavailable:
      description: Indisponibilidade temporária. Tente de novo com backoff.
      headers:
        Retry-After:
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
