openapi: 3.1.0
info:
  title: API DANFSe
  version: 1.0.0
  description: >-
    Contrato canônico e versionado da API DANFSe v1. Inclui conversões
    unitárias, trial autenticado e lotes síncronos. Os exemplos de erro usam
    Problem Details (RFC 9457) e nunca carregam XML, PDF, chave fiscal ou dado
    pessoal.
externalDocs:
  description: Guia humano e quickstarts seguros
  url: https://cmpsoftware.com.br/api-danfse/
servers:
  - url: https://api.cmpsoftware.com.br
x-api-version: v1
paths:
  /v1/danfse:
    post:
      operationId: convertDanfse
      summary: Gerar um PDF DANFSe com saldo da Conta
      security:
        - apiToken: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/xml:
            schema:
              type: string
              minLength: 1
              contentMediaType: application/xml
              maxLength: 1048576
      responses:
        '200':
          description: PDF DANFSe gerado com saldo da Conta
          headers:
            Request-Id:
              $ref: '#/components/headers/RequestId'
            Danfse-Renderer-Version:
              $ref: '#/components/headers/RendererVersion'
            Danfse-Normative-Version:
              $ref: '#/components/headers/NormativeVersion'
            Danfse-Xml-Version:
              $ref: '#/components/headers/XmlVersion'
            Idempotency-Replayed:
              description: Cabeçalho textual que indica se a resposta veio do replay idempotente.
              schema:
                type: string
                enum: ['true', 'false']
          content:
            application/pdf:
              schema:
                type: string
                contentEncoding: binary
        '400':
          $ref: '#/components/responses/Problem'
        '401':
          $ref: '#/components/responses/Problem'
        '402':
          description: Saldo insuficiente para uma Conversão DANFSe
          headers:
            Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
              example:
                type: https://cmpsoftware.com.br/erros/insufficient_balance
                title: Saldo insuficiente
                status: 402
                detail: A Conta não possui saldo para esta Conversão DANFSe.
                code: insufficient_balance
        '403':
          description: Conta não elegível para Conversão DANFSe
          headers:
            Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
              example:
                type: https://cmpsoftware.com.br/erros/account_not_eligible
                title: Conta não elegível
                status: 403
                detail: Esta Conta não está elegível para Conversão DANFSe.
                code: account_not_eligible
        '409':
          $ref: '#/components/responses/Problem'
        '413':
          $ref: '#/components/responses/Problem'
        '415':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
        '500':
          $ref: '#/components/responses/Problem'
  /v1/danfse/batch:
    post:
      operationId: convertDanfseBatch
      summary: Gerar lote de PDFs DANFSe e manifesto em ZIP com saldo da Conta
      description: Permite converter de 1 a 50 XMLs de NFS-e (cada um até 1 MiB, cada nome UTF-8 até 255 bytes e total descompactado até 8 MiB) por multipart/form-data ou application/zip, retornando um arquivo ZIP com manifesto.json e PDFs individuais gerados com sucesso.
      security:
        - apiToken: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        description: Lote de 1 a 50 arquivos XML (cada XML <= 1 MiB, cada nome UTF-8 <= 255 bytes e total descompactado <= 8 MiB).
        content:
          multipart/form-data:
            schema:
              type: object
              required: [files]
              properties:
                files:
                  type: array
                  description: Lista de 1 a 50 arquivos XML de origem (cada arquivo com até 1 MiB, nome UTF-8 até 255 bytes e total até 8 MiB).
                  items:
                    type: string
                    format: binary
                    maxLength: 1048576
                  minItems: 1
                  maxItems: 50
          application/zip:
            schema:
              type: string
              format: binary
              maxLength: 10485760
              description: Arquivo ZIP contendo de 1 a 50 arquivos XML na raiz (cada XML descompactado <= 1 MiB, cada nome UTF-8 <= 255 bytes, total descompactado <= 8 MiB). O corpo ZIP permanece limitado a 10 MiB.
      responses:
        '200':
          description: Arquivo ZIP contendo manifesto.json (conforme o esquema ManifestoLote) e PDFs individuais gerados com sucesso.
          headers:
            Request-Id:
              $ref: '#/components/headers/RequestId'
            Danfse-Renderer-Version:
              $ref: '#/components/headers/RendererVersion'
            Danfse-Normative-Version:
              $ref: '#/components/headers/NormativeVersion'
            Idempotency-Replayed:
              description: Cabeçalho textual que indica se a resposta veio do replay idempotente.
              schema:
                type: string
                enum: ['true', 'false']
          content:
            application/zip:
              schema:
                type: string
                format: binary
                contentEncoding: binary
                description: Arquivo ZIP contendo manifesto.json e PDFs individuais gerados com sucesso.
                x-manifesto-schema:
                  $ref: '#/components/schemas/ManifestoLote'
        '400':
          $ref: '#/components/responses/Problem'
        '401':
          $ref: '#/components/responses/Problem'
        '402':
          description: Saldo insuficiente para o lote de Conversões DANFSe
          headers:
            Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
              example:
                type: https://cmpsoftware.com.br/erros/insufficient_balance
                title: Saldo insuficiente
                status: 402
                detail: A Conta não possui saldo disponível para esta Conversão DANFSe.
                code: insufficient_balance
        '403':
          description: Conta não elegível para Conversão DANFSe
          headers:
            Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
              example:
                type: https://cmpsoftware.com.br/erros/account_not_eligible
                title: Conta não elegível
                status: 403
                detail: Esta Conta não está elegível para Conversão DANFSe.
                code: account_not_eligible
        '409':
          $ref: '#/components/responses/Problem'
        '413':
          $ref: '#/components/responses/Problem'
        '415':
          $ref: '#/components/responses/Problem'
        '500':
          $ref: '#/components/responses/Problem'
  /v1/danfse/trial:
    post:
      operationId: convertDanfseTrial
      summary: Gerar um PDF DANFSe de trial autenticado
      description: >-
        Disponível para uma Conta autenticada no máximo uma vez a cada 30 dias
        após um trial concluído com sucesso. O primeiro pagamento da Conta
        encerra permanentemente a elegibilidade ao trial. Quando o limite por
        Conta ou o pagamento anterior impedir a operação, a API retorna 403
        Problem Details com o código estável `trial_not_available`.
      security:
        - apiToken: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/xml:
            schema:
              type: string
              minLength: 1
              contentMediaType: application/xml
              maxLength: 1048576
      responses:
        '200':
          description: PDF DANFSe com marca d'água de trial
          headers:
            Request-Id:
              $ref: '#/components/headers/RequestId'
            Danfse-Renderer-Version:
              $ref: '#/components/headers/RendererVersion'
            Danfse-Normative-Version:
              $ref: '#/components/headers/NormativeVersion'
            Danfse-Xml-Version:
              $ref: '#/components/headers/XmlVersion'
            Idempotency-Replayed:
              description: Cabeçalho textual que indica se a resposta veio do replay idempotente.
              schema:
                type: string
                enum: ['true', 'false']
          content:
            application/pdf:
              schema:
                type: string
                contentEncoding: binary
        '400':
          $ref: '#/components/responses/Problem'
        '401':
          $ref: '#/components/responses/Problem'
        '403':
          description: Trial indisponível para esta Conta
          headers:
            Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
              example:
                type: https://cmpsoftware.com.br/erros/trial_not_available
                title: Trial indisponível
                status: 403
                detail: O trial está limitado a uma utilização por Conta a cada 30 dias e é encerrado após o primeiro pagamento.
                code: trial_not_available
        '409':
          $ref: '#/components/responses/Problem'
        '413':
          $ref: '#/components/responses/Problem'
        '415':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
        '500':
          $ref: '#/components/responses/Problem'
  /v1/danfse/demonstracao:
    post:
      operationId: convertDanfseDemonstracao
      summary: Gerar um PDF DANFSe de demonstração sem Conta
      security: []
      parameters:
        - name: X-Professional-Use
          in: header
          required: true
          description: Confirma uso profissional ou empresarial para a demonstração anônima; envie `confirmed`.
          schema:
            type: string
            const: confirmed
        - name: CF-Turnstile-Response
          in: header
          required: true
          description: Token Turnstile de uso único obtido pelo widget da demonstração web; não se aplica às rotas autenticadas.
          schema:
            type: string
            minLength: 1
            maxLength: 2048
        - name: X-Trial-Request-Id
          in: header
          required: false
          description: UUID v4 gerado pelo cliente para identificar a conversão; use o mesmo valor em DELETE /v1/danfse/demonstracao/{requestId} enquanto ela estiver em andamento.
          schema:
            type: string
            format: uuid
            pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$'
      requestBody:
        required: true
        content:
          application/xml:
            schema:
              type: string
              contentMediaType: application/xml
              maxLength: 1048576
      responses:
        '200':
          description: PDF DANFSe com marca d'água de demonstração
          headers:
            Request-Id:
              $ref: '#/components/headers/RequestId'
            Danfse-Renderer-Version:
              $ref: '#/components/headers/RendererVersion'
            Danfse-Normative-Version:
              $ref: '#/components/headers/NormativeVersion'
            Danfse-Xml-Version:
              $ref: '#/components/headers/XmlVersion'
          content:
            application/pdf:
              schema:
                type: string
                contentEncoding: binary
        '403':
          $ref: '#/components/responses/Problem'
        '409':
          $ref: '#/components/responses/Problem'
        '413':
          $ref: '#/components/responses/Problem'
        '415':
          $ref: '#/components/responses/Problem'
        '422':
          $ref: '#/components/responses/Problem'
        '500':
          $ref: '#/components/responses/Problem'
        '503':
          $ref: '#/components/responses/Problem'
  /v1/danfse/demonstracao/{requestId}:
    delete:
      operationId: cancelDanfseDemonstracao
      summary: Cancelar uma conversão de demonstração em andamento
      description: Aceita o UUID v4 informado em X-Trial-Request-Id e cancela a reserva enquanto a conversão estiver em andamento; depois de concluída, retorna 409.
      security: []
      parameters:
        - name: requestId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$'
      responses:
        '400':
          $ref: '#/components/responses/Problem'
        '204':
          description: Reserva cancelada ou já ausente
        '409':
          $ref: '#/components/responses/Problem'
        '503':
          $ref: '#/components/responses/Problem'
components:
  securitySchemes:
    apiToken:
      type: http
      scheme: bearer
      bearerFormat: opaque API Token
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: Válida por 24 horas. A mesma chave e entrada não consome outra Conversão DANFSe.
      schema:
        type: string
        minLength: 1
        maxLength: 255
  headers:
    RequestId:
      description: Identificador seguro para suporte.
      schema:
        type: string
        format: uuid
    RendererVersion:
      schema:
        type: string
    NormativeVersion:
      schema:
        type: string
    XmlVersion:
      schema:
        type: string
        enum: ['1.00', '1.01']
  responses:
    Problem:
      description: Erro seguro sem XML, PDF, chave fiscal, stack ou dado pessoal.
      headers:
        Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  schemas:
    Problem:
      type: object
      required: [type, title, status, detail, code]
      additionalProperties: false
      properties:
        type:
          type: string
          format: uri
        title:
          type: string
        status:
          type: integer
          minimum: 400
          maximum: 599
        detail:
          type: string
        code:
          type: string
    XmlEntrada:
      type: string
      minLength: 1
      maxLength: 1048576
      contentMediaType: application/xml
      description: XML UTF-8 bruto, limitado a 1 MiB.
    PdfDanfse:
      type: string
      format: binary
      contentMediaType: application/pdf
      description: PDF DANFSe retornado por uma conversão unitária.
    ZipLote:
      type: string
      format: binary
      contentMediaType: application/zip
      description: ZIP de retorno com manifesto.json e PDFs dos itens concluídos.
    LoteMultipart:
      type: object
      required: [files]
      properties:
        files:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: string
            format: binary
            maxLength: 1048576
          description: XMLs na raiz; cada nome tem até 255 bytes e o total descompactado até 8 MiB.
    ManifestoLote:
      type: object
      required: [items]
      description: Estrutura do manifesto.json presente no arquivo ZIP de retorno do lote.
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ItemManifestoLote'
    ItemManifestoLote:
      type: object
      required: [index, name, status]
      description: Resultado individual de conversão de um XML do lote.
      properties:
        index:
          type: integer
          minimum: 1
          maximum: 50
          description: Índice do item no lote (1 a 50).
        name:
          type: string
          description: Nome seguro do arquivo XML de origem.
        status:
          type: string
          enum: [success, error]
          description: Indica se a conversão do XML em PDF foi bem-sucedida ou falhou.
        file:
          type: string
          description: Nome do arquivo PDF correspondente dentro do arquivo ZIP quando o status for success.
        xmlVersion:
          type: string
          enum: ['1.00', '1.01']
          description: Versão do leiaute do XML identificada.
        rendererVersion:
          type: string
          description: Versão do renderizador utilizada.
        normativeVersion:
          type: string
          description: Versão normativa aplicada.
        error:
          $ref: '#/components/schemas/Problem'
