openapi: 3.0.3
info:
  title: EmiteHub — API de Documentos Fiscais
  version: "1.0.0-draft"
  description: |
    API REST para emissão de documentos fiscais eletrônicos. Esta versão
    cobre **MDF-e (58)**, **CT-e (57)**, **NF-e (55)**, **NFC-e (65)** e **NFS-e (padrão nacional)**; demais documentos e conectores
    entram em incrementos. Versão em rascunho: endpoints e
    campos podem mudar até a publicação oficial.

    ## Autenticação
    Header `X-API-Key: emitehub_live_<id>_<segredo>` (sandbox: `emitehub_test_...`).
    A chave é secreta; use apenas de servidor para servidor.

    ## Emissão assíncrona
    `POST` de emissão responde `202` com `id`. O resultado chega por webhook
    e por `GET`. `Idempotency-Key` é **obrigatório** nas emissões: repetir a
    mesma chave devolve o mesmo documento, sem duplicar a nota.

    ## Limites
    Por plano (Starter 60 req/min). Ao exceder: `429` + `Retry-After`.

    ## Convenções
    JSON UTF-8; datas ISO 8601 UTC; campo sem valor = `null`; novos campos
    podem surgir na v1 (ignore os desconhecidos).

servers:
  - url: https://api.emitehub.com/v1
    description: Produção (domínio a definir após o naming)
  - url: https://sandbox.api.emitehub.com/v1
    description: Sandbox (homologação SEFAZ)

security:
  - ApiKeyAuth: []

tags:
  - name: Emitentes
  - name: MDF-e
  - name: CT-e
  - name: NF-e
  - name: NFC-e
  - name: NFS-e
  - name: SEFAZ
  - name: Webhooks

paths:
  /issuers:
    get:
      tags: [Emitentes]
      operationId: listIssuers
      summary: Listar emitentes
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Página de emitentes.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/Issuer" } }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    post:
      tags: [Emitentes]
      operationId: createIssuer
      summary: Cadastrar emitente
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/IssuerInput" }
      responses:
        "201":
          description: Emitente criado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Issuer" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/Conflict" }

  /issuers/{issuerId}:
    parameters:
      - $ref: "#/components/parameters/IssuerId"
    get:
      tags: [Emitentes]
      operationId: getIssuer
      summary: Detalhar emitente
      responses:
        "200":
          description: Emitente.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Issuer" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /issuers/{issuerId}/certificate:
    parameters:
      - $ref: "#/components/parameters/IssuerId"
    put:
      tags: [Emitentes]
      operationId: uploadCertificate
      summary: Enviar certificado A1
      description: |
        Envia o `.pfx` e a senha. O arquivo é validado (validade, cadeia
        ICP-Brasil, CNPJ igual ao do emitente) e armazenado criptografado.
        **O arquivo nunca é devolvido.**
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file, password]
              properties:
                file: { type: string, format: binary }
                password: { type: string, format: password }
      responses:
        "200":
          description: Certificado aceito.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CertificateInfo" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    get:
      tags: [Emitentes]
      operationId: getCertificate
      summary: Metadados do certificado
      responses:
        "200":
          description: Metadados.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CertificateInfo" }
        "404": { $ref: "#/components/responses/NotFound" }

  /issuers/{issuerId}/nfce-csc:
    parameters:
      - $ref: "#/components/parameters/IssuerId"
    put:
      tags: [Emitentes]
      operationId: setNfceCsc
      summary: Cadastrar o CSC da NFC-e (por ambiente)
      description: O valor do CSC é guardado cifrado e nunca é devolvido pela API.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [environment, id, csc]
              properties:
                environment: { $ref: "#/components/schemas/Environment" }
                id: { type: string, description: "Identificador do CSC (até 6 dígitos)." }
                csc: { type: string, description: "Código de Segurança do Contribuinte." }
      responses:
        "200": { description: CSC gravado. }
        "400": { $ref: "#/components/responses/BadRequest" }
        "404": { $ref: "#/components/responses/NotFound" }

  /sefaz/status:
    get:
      tags: [SEFAZ]
      operationId: sefazStatus
      summary: Status do serviço da SEFAZ
      parameters:
        - { name: uf, in: query, schema: { type: string, minLength: 2, maxLength: 2 }, example: SP }
        - { name: model, in: query, schema: { type: string, enum: ["55", "57", "58", "65"] } }
        - { name: environment, in: query, schema: { $ref: "#/components/schemas/Environment" } }
      responses:
        "200":
          description: Situação do serviço.
          content:
            application/json:
              schema:
                type: object
                properties:
                  uf: { type: string }
                  available: { type: boolean }
                  sefazCode: { type: string, nullable: true }
                  message: { type: string, nullable: true }
                  checkedAt: { type: string, format: date-time }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /cte:
    get:
      tags: [CT-e]
      operationId: listCte
      summary: Listar CT-e
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/UpdatedSince"
        - { name: issuerId, in: query, schema: { type: string } }
        - { name: status, in: query, schema: { $ref: "#/components/schemas/DocStatus" } }
        - { name: accessKey, in: query, schema: { type: string } }
        - { name: externalRef, in: query, schema: { type: string } }
      responses:
        "200":
          description: Página de CT-e.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/FiscalDocument" } }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    post:
      tags: [CT-e]
      operationId: createCte
      summary: Emitir CT-e
      description: "CT-e modelo 57 (layout 4.00), modal rodoviário. Tipos: normal e complemento de valores."
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CteInput" }
      responses:
        "202": { $ref: "#/components/responses/Accepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { description: "Chave de teste tentando emitir em produção." }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /cte/{id}:
    parameters:
      - $ref: "#/components/parameters/DocId"
    get:
      tags: [CT-e]
      operationId: getCte
      summary: Detalhar CT-e
      responses:
        "200":
          description: CT-e.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FiscalDocument" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /cte/{id}/refresh:
    parameters:
      - $ref: "#/components/parameters/DocId"
    post:
      tags: [CT-e]
      operationId: refreshCte
      summary: Reconciliar com a SEFAZ (consulta pela chave e atualiza o estado)
      responses:
        "200":
          description: Documento atualizado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FiscalDocument" }
        "404": { $ref: "#/components/responses/NotFound" }

  /cte/{id}/xml:
    parameters:
      - $ref: "#/components/parameters/DocId"
    get:
      tags: [CT-e]
      operationId: getCteXml
      summary: URL temporária do XML autorizado
      responses:
        "200": { $ref: "#/components/responses/FileUrl" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /cte/{id}/dacte:
    parameters:
      - $ref: "#/components/parameters/DocId"
    get:
      tags: [CT-e]
      operationId: getCtePdf
      summary: URL temporária do DACTE (PDF)
      responses:
        "200": { $ref: "#/components/responses/FileUrl" }
        "404": { $ref: "#/components/responses/NotFound" }

  /cte/{id}/cancel:
    parameters:
      - $ref: "#/components/parameters/DocId"
    post:
      tags: [CT-e]
      operationId: cancelCte
      summary: Cancelar CT-e
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Justification" }
      responses:
        "202": { $ref: "#/components/responses/Accepted" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /cte/{id}/correction:
    parameters:
      - $ref: "#/components/parameters/DocId"
    post:
      tags: [CT-e]
      operationId: correctCte
      summary: Carta de correção (CC-e)
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [corrections]
              properties:
                corrections:
                  type: array
                  minItems: 1
                  items:
                    type: object
                    required: [group, field, value]
                    properties:
                      group: { type: string, description: "Grupo do campo (ex.: ide)." }
                      field: { type: string, description: "Campo (ex.: xDetRetira)." }
                      value: { type: string, maxLength: 500 }
                      itemNumber: { type: integer }
      responses:
        "202": { $ref: "#/components/responses/Accepted" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /nfe:
    get:
      tags: [NF-e]
      operationId: listNfe
      summary: Listar NF-e
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/UpdatedSince"
        - { name: issuerId, in: query, schema: { type: string } }
        - { name: status, in: query, schema: { $ref: "#/components/schemas/DocStatus" } }
        - { name: accessKey, in: query, schema: { type: string } }
        - { name: externalRef, in: query, schema: { type: string } }
      responses:
        "200":
          description: Página de NF-e.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/FiscalDocument" } }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    post:
      tags: [NF-e]
      operationId: createNfe
      summary: Emitir NF-e
      description: "NF-e modelo 55 (layout 4.00). Totais são calculados a partir dos itens."
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NfeInput" }
      responses:
        "202": { $ref: "#/components/responses/Accepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { description: "Chave de teste tentando emitir em produção." }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /nfe/{id}:
    parameters:
      - $ref: "#/components/parameters/DocId"
    get:
      tags: [NF-e]
      operationId: getNfe
      summary: Detalhar NF-e
      responses:
        "200":
          description: NF-e.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FiscalDocument" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /nfe/{id}/refresh:
    parameters:
      - $ref: "#/components/parameters/DocId"
    post:
      tags: [NF-e]
      operationId: refreshNfe
      summary: Reconciliar com a SEFAZ (consulta pela chave e atualiza o estado)
      responses:
        "200":
          description: Documento atualizado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FiscalDocument" }
        "404": { $ref: "#/components/responses/NotFound" }

  /nfe/{id}/xml:
    parameters:
      - $ref: "#/components/parameters/DocId"
    get:
      tags: [NF-e]
      operationId: getNfeXml
      summary: URL temporária do XML autorizado
      responses:
        "200": { $ref: "#/components/responses/FileUrl" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /nfe/{id}/danfe:
    parameters:
      - $ref: "#/components/parameters/DocId"
    get:
      tags: [NF-e]
      operationId: getNfePdf
      summary: URL temporária do DANFE (PDF)
      responses:
        "200": { $ref: "#/components/responses/FileUrl" }
        "404": { $ref: "#/components/responses/NotFound" }

  /nfe/{id}/cancel:
    parameters:
      - $ref: "#/components/parameters/DocId"
    post:
      tags: [NF-e]
      operationId: cancelNfe
      summary: Cancelar NF-e
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Justification" }
      responses:
        "202": { $ref: "#/components/responses/Accepted" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /nfe/{id}/correction:
    parameters:
      - $ref: "#/components/parameters/DocId"
    post:
      tags: [NF-e]
      operationId: correctNfe
      summary: Carta de correção (CC-e)
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [correctionText]
              properties:
                correctionText: { type: string, minLength: 15, maxLength: 1000 }
      responses:
        "202": { $ref: "#/components/responses/Accepted" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /nfce:
    get:
      tags: [NFC-e]
      operationId: listNfce
      summary: Listar NFC-e
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/UpdatedSince"
        - { name: issuerId, in: query, schema: { type: string } }
        - { name: status, in: query, schema: { $ref: "#/components/schemas/DocStatus" } }
        - { name: accessKey, in: query, schema: { type: string } }
        - { name: externalRef, in: query, schema: { type: string } }
      responses:
        "200":
          description: Página de NFC-e.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/FiscalDocument" } }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    post:
      tags: [NFC-e]
      operationId: createNfce
      summary: Emitir NFC-e
      description: "NFC-e modelo 65 (layout 4.00), emissão online. Requer o CSC do emitente (PUT /issuers/{issuerId}/nfce-csc) quando o QR Code é v2."
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NfeInput" }
      responses:
        "202": { $ref: "#/components/responses/Accepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { description: "Chave de teste tentando emitir em produção." }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /nfce/{id}:
    parameters:
      - $ref: "#/components/parameters/DocId"
    get:
      tags: [NFC-e]
      operationId: getNfce
      summary: Detalhar NFC-e
      responses:
        "200":
          description: NFC-e.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FiscalDocument" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /nfce/{id}/refresh:
    parameters:
      - $ref: "#/components/parameters/DocId"
    post:
      tags: [NFC-e]
      operationId: refreshNfce
      summary: Reconciliar com a SEFAZ (consulta pela chave e atualiza o estado)
      responses:
        "200":
          description: Documento atualizado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FiscalDocument" }
        "404": { $ref: "#/components/responses/NotFound" }

  /nfce/{id}/xml:
    parameters:
      - $ref: "#/components/parameters/DocId"
    get:
      tags: [NFC-e]
      operationId: getNfceXml
      summary: URL temporária do XML autorizado
      responses:
        "200": { $ref: "#/components/responses/FileUrl" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /nfce/{id}/danfe:
    parameters:
      - $ref: "#/components/parameters/DocId"
    get:
      tags: [NFC-e]
      operationId: getNfcePdf
      summary: URL temporária do DANFE (PDF)
      responses:
        "200": { $ref: "#/components/responses/FileUrl" }
        "404": { $ref: "#/components/responses/NotFound" }

  /nfce/{id}/cancel:
    parameters:
      - $ref: "#/components/parameters/DocId"
    post:
      tags: [NFC-e]
      operationId: cancelNfce
      summary: Cancelar NFC-e
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Justification" }
      responses:
        "202": { $ref: "#/components/responses/Accepted" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /nfse:
    get:
      tags: [NFS-e]
      operationId: listNfse
      summary: Listar NFS-e (padrão nacional)
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/UpdatedSince"
        - { name: issuerId, in: query, schema: { type: string } }
        - { name: status, in: query, schema: { $ref: "#/components/schemas/DocStatus" } }
        - { name: accessKey, in: query, schema: { type: string } }
        - { name: externalRef, in: query, schema: { type: string } }
      responses:
        "200":
          description: Página de NFS-e (padrão nacional).
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/FiscalDocument" } }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    post:
      tags: [NFS-e]
      operationId: createNfse
      summary: Emitir NFS-e (padrão nacional)
      description: "NFS-e pelo padrão nacional (DPS). O município do prestador precisa ter aderido ao padrão nacional. Antes da autorização, `accessKey` é o Id da DPS; depois, a chave de 50 dígitos."
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NfseInput" }
      responses:
        "202": { $ref: "#/components/responses/Accepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { description: "Chave de teste tentando emitir em produção." }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /nfse/{id}:
    parameters:
      - $ref: "#/components/parameters/DocId"
    get:
      tags: [NFS-e]
      operationId: getNfse
      summary: Detalhar NFS-e (padrão nacional)
      responses:
        "200":
          description: NFS-e (padrão nacional).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FiscalDocument" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /nfse/{id}/refresh:
    parameters:
      - $ref: "#/components/parameters/DocId"
    post:
      tags: [NFS-e]
      operationId: refreshNfse
      summary: Reconciliar com a SEFAZ (consulta pela chave e atualiza o estado)
      responses:
        "200":
          description: Documento atualizado.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FiscalDocument" }
        "404": { $ref: "#/components/responses/NotFound" }

  /nfse/{id}/xml:
    parameters:
      - $ref: "#/components/parameters/DocId"
    get:
      tags: [NFS-e]
      operationId: getNfseXml
      summary: URL temporária do XML autorizado
      responses:
        "200": { $ref: "#/components/responses/FileUrl" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /nfse/{id}/danfse:
    parameters:
      - $ref: "#/components/parameters/DocId"
    get:
      tags: [NFS-e]
      operationId: getNfsePdf
      summary: URL temporária do DANFSE (PDF)
      responses:
        "200": { $ref: "#/components/responses/FileUrl" }
        "404": { $ref: "#/components/responses/NotFound" }

  /nfse/{id}/cancel:
    parameters:
      - $ref: "#/components/parameters/DocId"
    post:
      tags: [NFS-e]
      operationId: cancelNfse
      summary: Cancelar NFS-e (padrão nacional)
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: 
              type: object
              required: [justification]
              properties:
                reasonCode: { type: string, enum: ["1", "2", "9"], default: "1", description: "1 erro na emissão, 2 serviço não prestado, 9 outros." }
                justification: { type: string, minLength: 15, maxLength: 255 }
      responses:
        "202": { $ref: "#/components/responses/Accepted" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /mdfe:
    get:
      tags: [MDF-e]
      operationId: listMdfe
      summary: Listar MDF-e
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/UpdatedSince"
        - { name: issuerId, in: query, schema: { type: string } }
        - { name: status, in: query, schema: { $ref: "#/components/schemas/DocStatus" } }
        - { name: accessKey, in: query, schema: { type: string, pattern: '^\d{44}$' } }
      responses:
        "200":
          description: Página de MDF-e.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/FiscalDocument" } }
                  pagination: { $ref: "#/components/schemas/Pagination" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [MDF-e]
      operationId: createMdfe
      summary: Emitir MDF-e
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MdfeInput" }
      responses:
        "202": { $ref: "#/components/responses/Accepted" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /mdfe/open:
    get:
      tags: [MDF-e]
      operationId: listOpenMdfe
      summary: MDF-e não encerrados do emitente
      parameters:
        - { name: issuerId, in: query, required: true, schema: { type: string } }
      responses:
        "200":
          description: Lista de MDF-e abertos.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/FiscalDocument" } }

  /mdfe/{id}:
    parameters:
      - $ref: "#/components/parameters/DocId"
    get:
      tags: [MDF-e]
      operationId: getMdfe
      summary: Detalhar MDF-e
      responses:
        "200":
          description: MDF-e.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FiscalDocument" }
        "404": { $ref: "#/components/responses/NotFound" }

  /mdfe/{id}/xml:
    parameters:
      - $ref: "#/components/parameters/DocId"
    get:
      tags: [MDF-e]
      operationId: getMdfeXml
      summary: URL temporária do XML autorizado
      responses:
        "200": { $ref: "#/components/responses/FileUrl" }
        "404": { $ref: "#/components/responses/NotFound" }

  /mdfe/{id}/damdfe:
    parameters:
      - $ref: "#/components/parameters/DocId"
    get:
      tags: [MDF-e]
      operationId: getDamdfe
      summary: URL temporária do DAMDFE (PDF)
      responses:
        "200": { $ref: "#/components/responses/FileUrl" }
        "404": { $ref: "#/components/responses/NotFound" }

  /mdfe/{id}/close:
    parameters:
      - $ref: "#/components/parameters/DocId"
    post:
      tags: [MDF-e]
      operationId: closeMdfe
      summary: Encerrar MDF-e
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                closedAt: { type: string, format: date-time, description: "Padrão: agora." }
                uf: { type: string, minLength: 2, maxLength: 2, description: "UF de encerramento." }
                cityCode: { type: string, description: "Código IBGE (7 dígitos) do município de encerramento.", example: "3170206" }
      responses:
        "202": { $ref: "#/components/responses/Accepted" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /mdfe/{id}/cancel:
    parameters:
      - $ref: "#/components/parameters/DocId"
    post:
      tags: [MDF-e]
      operationId: cancelMdfe
      summary: Cancelar MDF-e
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Justification" }
      responses:
        "202": { $ref: "#/components/responses/Accepted" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /mdfe/{id}/drivers:
    parameters:
      - $ref: "#/components/parameters/DocId"
    post:
      tags: [MDF-e]
      operationId: addMdfeDriver
      summary: Incluir condutor
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Driver" }
      responses:
        "202": { $ref: "#/components/responses/Accepted" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /mdfe/{id}/documents:
    parameters:
      - $ref: "#/components/parameters/DocId"
    post:
      tags: [MDF-e]
      operationId: addMdfeDocuments
      summary: Incluir DF-e (NF-e/CT-e) no MDF-e
      description: Inclui documentos em um município de descarga (existente ou novo) de um MDF-e autorizado e não encerrado.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Unloading" }
      responses:
        "202": { $ref: "#/components/responses/Accepted" }
        "422": { $ref: "#/components/responses/Unprocessable" }

  /webhooks:
    get:
      tags: [Webhooks]
      operationId: listWebhooks
      summary: Listar webhooks
      responses:
        "200":
          description: Webhooks cadastrados.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/Webhook" } }
    post:
      tags: [Webhooks]
      operationId: createWebhook
      summary: Cadastrar webhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  items: { $ref: "#/components/schemas/WebhookEvent" }
      responses:
        "201":
          description: Criado. O `secret` (HMAC) aparece só nesta resposta.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Webhook" }

  /webhooks/{webhookId}:
    delete:
      tags: [Webhooks]
      operationId: deleteWebhook
      summary: Remover webhook
      parameters:
        - { name: webhookId, in: path, required: true, schema: { type: string } }
      responses:
        "204": { description: Removido. }
        "404": { $ref: "#/components/responses/NotFound" }

  /webhooks/deliveries:
    get:
      tags: [Webhooks]
      operationId: listWebhookDeliveries
      summary: Entregas de webhook
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
        - { name: status, in: query, schema: { type: string, enum: [pending, delivered, failed] } }
      responses:
        "200":
          description: Entregas.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { $ref: "#/components/schemas/WebhookDelivery" } }
                  pagination: { $ref: "#/components/schemas/Pagination" }

  /webhooks/deliveries/{deliveryId}/retry:
    post:
      tags: [Webhooks]
      operationId: retryWebhookDelivery
      summary: Reenviar entrega
      parameters:
        - { name: deliveryId, in: path, required: true, schema: { type: string } }
      responses:
        "202": { description: Reenvio agendado. }
        "404": { $ref: "#/components/responses/NotFound" }

x-webhooks:
  documentEvent:
    post:
      summary: Evento de documento fiscal
      description: |
        Enviado ao `url` cadastrado. Cabeçalho `X-EmiteHub-Signature: sha256=<HMAC-SHA256 do corpo>`
        calculado com o `secret` do webhook. Responda `2xx` em até 10 s; caso contrário
        há novas tentativas com espera progressiva.
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookPayload" }
      responses:
        "200": { description: Recebido. }

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Chave `emitehub_live_...` ou `emitehub_test_...`. Secreta.

  parameters:
    Page:
      name: page
      in: query
      schema: { type: integer, minimum: 1, default: 1 }
    Limit:
      name: limit
      in: query
      description: Itens por página (máx. 100).
      schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
    UpdatedSince:
      name: updatedSince
      in: query
      description: Criados/alterados desde (inclusivo), ISO 8601 UTC. Use `upsert` por `id` ao sincronizar.
      schema: { type: string, format: date-time }
    IssuerId:
      name: issuerId
      in: path
      required: true
      schema: { type: string }
    DocId:
      name: id
      in: path
      required: true
      schema: { type: string }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: "Valor único por operação (ex.: UUID). Repetir devolve o mesmo resultado."
      schema: { type: string, minLength: 8, maxLength: 128 }

  headers:
    RateLimit-Limit:
      schema: { type: integer }
      description: Requisições permitidas por minuto.
    RateLimit-Remaining:
      schema: { type: integer }
      description: Restantes no minuto atual.
    RateLimit-Reset:
      schema: { type: integer }
      description: Segundos até reiniciar o contador.

  schemas:
    Environment:
      type: string
      enum: [production, homologation]

    DocStatus:
      type: string
      description: Novos valores podem surgir.
      enum: [processing, authorized, rejected, denied, contingency, cancelled, closed]

    Pagination:
      type: object
      required: [page, limit, total, hasNext]
      properties:
        page: { type: integer }
        limit: { type: integer }
        total: { type: integer }
        hasNext: { type: boolean }

    Address:
      type: object
      required: [street, number, district, cityCode, cityName, uf, zipCode]
      properties:
        street: { type: string }
        number: { type: string }
        complement: { type: string, nullable: true }
        district: { type: string }
        cityCode: { type: string, description: "Código IBGE do município (7 dígitos).", example: "3550308" }
        cityName: { type: string }
        uf: { type: string, minLength: 2, maxLength: 2 }
        zipCode: { type: string, pattern: '^\d{8}$' }

    IssuerInput:
      type: object
      required: [cnpj, legalName, stateRegistration, taxRegime, address]
      properties:
        cnpj: { type: string, pattern: '^\d{14}$' }
        legalName: { type: string }
        tradeName: { type: string, nullable: true }
        stateRegistration: { type: string }
        taxRegime:
          type: string
          description: CRT. 1 = Simples Nacional, 2 = Simples excesso de sublimite, 3 = Regime normal.
          enum: ["1", "2", "3"]
        rntrc: { type: string, nullable: true, description: "Registro ANTT (transportadores)." }
        address: { $ref: "#/components/schemas/Address" }
        nfeSeries: { type: integer, default: 1 }
        mdfeSeries: { type: integer, default: 1 }

    Issuer:
      allOf:
        - $ref: "#/components/schemas/IssuerInput"
        - type: object
          required: [id, hasCertificate, createdAt]
          properties:
            id: { type: string }
            hasCertificate: { type: boolean }
            createdAt: { type: string, format: date-time }

    CertificateInfo:
      type: object
      properties:
        subject: { type: string }
        cnpj: { type: string }
        notBefore: { type: string, format: date-time }
        notAfter: { type: string, format: date-time }
        daysToExpire: { type: integer }

    Justification:
      type: object
      required: [justification]
      properties:
        justification: { type: string, minLength: 15, maxLength: 255 }

    PartyAddress:
      type: object
      required: [street, number, district, cityCode, cityName, uf]
      properties:
        street: { type: string }
        number: { type: string, description: "Use S/N quando não houver." }
        complement: { type: string, nullable: true }
        district: { type: string }
        cityCode: { type: string, description: "IBGE, 7 dígitos.", example: "3550308" }
        cityName: { type: string }
        uf: { type: string, minLength: 2, maxLength: 2 }
        zipCode: { type: string, description: "8 dígitos.", nullable: true }
    Party:
      type: object
      required: [name, address]
      description: Parte do CT-e (remetente, expedidor, recebedor, destinatário ou tomador).
      properties:
        cnpj: { type: string, nullable: true }
        cpf: { type: string, nullable: true }
        stateRegistration: { type: string, nullable: true, description: "IE (dígitos) ou ISENTO." }
        name: { type: string, maxLength: 60 }
        tradeName: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        email: { type: string, nullable: true }
        address: { $ref: "#/components/schemas/PartyAddress" }
    CteInput:
      type: object
      required: [issuerId, cfop, natureOfOperation, origin, destination, taker, freight, tax, cargo, documents]
      properties:
        issuerId: { type: string }
        environment: { $ref: "#/components/schemas/Environment" }
        externalRef: { type: string, nullable: true }
        cteType: { type: string, enum: [normal, complement], default: normal }
        complementOf: { type: array, items: { type: string }, description: "Chaves dos CT-e complementados (cteType=complement)." }
        cfop: { type: string, example: "6353", description: "Prestação dentro da UF: 5xxx; outra UF: 6xxx; exterior: 7xxx." }
        natureOfOperation: { type: string, maxLength: 60, example: PRESTACAO DE SERVICO DE TRANSPORTE }
        serviceType: { type: string, enum: [normal, subcontracting, redispatch, intermediate_redispatch, multimodal], default: normal }
        issuedIn: { type: object, nullable: true, properties: { ibgeCode: { type: string }, name: { type: string }, uf: { type: string } }, description: "Município de emissão (padrão: o do emitente)." }
        origin: { $ref: "#/components/schemas/CteCity" }
        destination: { $ref: "#/components/schemas/CteCity" }
        pickupAtDestination: { type: boolean, default: false }
        pickupDetail: { type: string, nullable: true }
        taker:
          type: object
          required: [role, stateRegistrationIndicator]
          properties:
            role: { type: string, enum: [sender, shipper, consignee, recipient, other] }
            party: { type: object, allOf: [{ $ref: "#/components/schemas/Party" }], nullable: true, description: "Obrigatório quando role=other." }
            stateRegistrationIndicator: { type: string, enum: [contributor, exempt, non_contributor] }
        sender: { type: object, allOf: [{ $ref: "#/components/schemas/Party" }], nullable: true }
        shipper: { type: object, allOf: [{ $ref: "#/components/schemas/Party" }], nullable: true }
        consignee: { type: object, allOf: [{ $ref: "#/components/schemas/Party" }], nullable: true }
        recipient: { type: object, allOf: [{ $ref: "#/components/schemas/Party" }], nullable: true }
        freight:
          type: object
          required: [total]
          properties:
            total: { type: number, description: "Valor total da prestação." }
            received: { type: number, description: "Valor a receber (padrão: total)." }
            components: { type: array, items: { type: object, required: [name, amount], properties: { name: { type: string, maxLength: 15 }, amount: { type: number } } }, description: "A soma deve ser igual ao total." }
        tax:
          type: object
          required: [cst]
          description: |
            Tributação do ICMS. `cst`: 00, 20, 40/41/51, 60, 90, other_uf ou simples. Campos por CST:
            00 → baseAmount, rate, amount; 20 → + reductionPercent, exemptedAmount, benefitCode; 60 → baseRetained, amountRetained, rateRetained;
            90/other_uf → baseAmount, rate, amount (+ opcionais).
          additionalProperties: true
          properties:
            cst: { type: string, enum: ["00", "20", "40", "41", "51", "60", "90", other_uf, simples] }
        totalTaxesApprox: { type: number, nullable: true }
        fiscalInfo: { type: string, nullable: true }
        cargo:
          type: object
          required: [productName, quantities]
          properties:
            totalValue: { type: number, nullable: true }
            productName: { type: string, maxLength: 60 }
            otherCharacteristics: { type: string, nullable: true }
            quantities: { type: array, minItems: 1, items: { type: object, required: [unit, measure, amount], properties: { unit: { type: string, enum: ["00", "01", "02", "03", "04", "05"], description: "00 m³, 01 kg, 02 t, 03 unidades, 04 litros, 05 mmbtu." }, measure: { type: string, example: PESO BRUTO }, amount: { type: number } } } }
        documents:
          type: object
          properties:
            nfeKeys: { type: array, items: { type: string }, description: "Chaves de NF-e (44 dígitos). Obrigatório no tipo normal." }
        rntrc: { type: string, nullable: true, description: "Padrão: o do emitente." }
        authorizedDownloaders: { type: array, items: { type: string } }
        additionalInfo: { type: string, nullable: true }
        contributorNotes: { type: array, items: { type: string, maxLength: 160 }, maxItems: 10 }
    CteCity:
      type: object
      required: [ibgeCode, name, uf]
      properties: { ibgeCode: { type: string }, name: { type: string }, uf: { type: string } }
    NfeRecipient:
      type: object
      required: [stateRegistrationIndicator]
      properties:
        cnpj: { type: string, nullable: true }
        cpf: { type: string, nullable: true }
        foreignId: { type: string, nullable: true }
        name: { type: string, nullable: true }
        address: { type: object, allOf: [{ $ref: "#/components/schemas/PartyAddress" }], nullable: true }
        stateRegistrationIndicator: { type: string, enum: [contributor, exempt, non_contributor] }
        stateRegistration: { type: string, nullable: true }
        email: { type: string, nullable: true }
    NfeItem:
      type: object
      required: [code, description, ncm, cfop, unit, quantity, unitPrice, icms, pis, cofins]
      properties:
        code: { type: string }
        description: { type: string, maxLength: 120 }
        ncm: { type: string, description: "8 dígitos." }
        cest: { type: string, nullable: true }
        cfop: { type: string }
        unit: { type: string, maxLength: 6 }
        quantity: { type: number }
        unitPrice: { type: number }
        gtin: { type: string, nullable: true, description: "8/12/13/14 dígitos ou SEM GTIN." }
        freight: { type: number, nullable: true }
        insurance: { type: number, nullable: true }
        discount: { type: number, nullable: true }
        otherExpenses: { type: number, nullable: true }
        countsInTotal: { type: boolean, default: true }
        orderNumber: { type: string, nullable: true }
        approxTaxes: { type: number, nullable: true }
        icms:
          type: object
          required: [origin]
          additionalProperties: true
          description: |
            Grupo de ICMS: `origin` (0–8) e **`cst`** (regime normal: 00, 10, 20, 30, 40, 41, 50, 51, 60, 70, 90…) ou **`csosn`** (Simples: 101, 102, 103, 201, 202, 203, 300, 400, 500, 900),
            mais os campos oficiais do grupo (modBC, vBC, pICMS, vICMS, modBCST, vBCST, pICMSST, vICMSST, pCredSN, vCredICMSSN…). Valores numéricos aceitam número.
          properties:
            origin: { type: string }
            cst: { type: string }
            csosn: { type: string }
        ipi: { type: object, nullable: true, additionalProperties: true, description: "enq, cst (00/49/50/99 tributado; 01–05/51–55 não tributado), vBC, pIPI, vIPI." }
        pis: { $ref: "#/components/schemas/NfePisCofins" }
        cofins: { $ref: "#/components/schemas/NfePisCofins" }
    NfePisCofins:
      type: object
      required: [cst]
      properties:
        cst: { type: string }
        vBC: { type: number }
        rate: { type: number, description: "Alíquota em %." }
        amount: { type: number }
        qBCProd: { type: number }
        vAliqProd: { type: number }
    NfeInput:
      type: object
      required: [issuerId, operationNature, items, payments]
      description: NF-e (55) e NFC-e (65, sem destinatário obrigatório). Os totais são calculados a partir dos itens.
      properties:
        issuerId: { type: string }
        environment: { $ref: "#/components/schemas/Environment" }
        externalRef: { type: string, nullable: true }
        operationNature: { type: string, maxLength: 60, example: VENDA DE MERCADORIA }
        direction: { type: string, enum: [outbound, inbound], default: outbound }
        purpose: { type: string, enum: [normal, complement, adjustment, return], default: normal }
        finalConsumer: { type: boolean }
        presence: { type: string, enum: [not_applicable, in_person, internet, phone, home_delivery, in_person_offsite, other] }
        issuedAt: { type: string, format: date-time }
        exitAt: { type: string, format: date-time, nullable: true }
        referencedKeys: { type: array, items: { type: string } }
        recipient: { type: object, allOf: [{ $ref: "#/components/schemas/NfeRecipient" }], nullable: true }
        items: { type: array, minItems: 1, maxItems: 990, items: { $ref: "#/components/schemas/NfeItem" } }
        payments:
          type: array
          minItems: 1
          items:
            type: object
            required: [method, amount]
            properties:
              method: { type: string, example: "15", description: "Tabela tPag (01 dinheiro, 03/04 cartão, 15 boleto, 17 PIX, 90 sem pagamento, 99 outros…)." }
              amount: { type: number }
              description: { type: string, nullable: true }
              deferred: { type: boolean }
              card: { type: object, nullable: true, properties: { integrated: { type: boolean }, cnpj: { type: string }, brand: { type: string }, authCode: { type: string } } }
        changeAmount: { type: number, nullable: true }
        transport: { type: object, nullable: true, additionalProperties: true, description: "freightMode (0–4, 9), carrier, vehicle, volumes." }
        billing: { type: object, nullable: true, additionalProperties: true, description: "number, originalAmount, discount, netAmount, installments[]." }
        taxpayerInfo: { type: string, nullable: true }
        fiscalInfo: { type: string, nullable: true }
        authorizedDownloaders: { type: array, items: { type: string } }
    NfseInput:
      type: object
      required: [issuerId, competenceDate, service, values]
      properties:
        issuerId: { type: string }
        environment: { $ref: "#/components/schemas/Environment" }
        externalRef: { type: string, nullable: true }
        competenceDate: { type: string, format: date, description: "Não pode ser futura." }
        regime:
          type: object
          description: Se omitido, é derivado do cadastro do emitente.
          properties:
            simplesNacional: { type: string, enum: [not_optant, mei, me_epp] }
            simplesCalculation: { type: string, enum: ["1", "2", "3"] }
            special: { type: string, enum: ["0", "1", "2", "3", "4", "5", "6", "9"] }
        customer:
          type: object
          nullable: true
          required: [name]
          properties:
            cnpj: { type: string, nullable: true }
            cpf: { type: string, nullable: true }
            foreignId: { type: string, nullable: true }
            name: { type: string }
            phone: { type: string, nullable: true }
            email: { type: string, nullable: true }
            municipalRegistration: { type: string, nullable: true }
            address: { type: object, nullable: true, properties: { street: { type: string }, number: { type: string }, complement: { type: string }, district: { type: string }, cityCode: { type: string }, zipCode: { type: string } } }
        service:
          type: object
          required: [municipalityCode, nationalTaxCode, description]
          properties:
            municipalityCode: { type: string, description: "Município da prestação (IBGE, 7 dígitos)." }
            nationalTaxCode: { type: string, description: "6 dígitos (ex.: 010701).", example: "010701" }
            municipalTaxCode: { type: string, nullable: true }
            nbs: { type: string, nullable: true }
            description: { type: string, maxLength: 2000 }
        values:
          type: object
          required: [serviceAmount, issTaxation, issWithholding]
          properties:
            serviceAmount: { type: number }
            unconditionalDiscount: { type: number, nullable: true }
            conditionalDiscount: { type: number, nullable: true }
            issTaxation: { type: string, enum: ["1", "2", "3", "4"], description: "1 tributável, 2 imunidade, 3 exportação, 4 não incidência." }
            issWithholding: { type: string, enum: ["1", "2", "3"], description: "1 não retido, 2 retido pelo tomador, 3 retido pelo intermediário." }
            issRate: { type: number, nullable: true, description: "% (2 a 5); dispensada para optantes do Simples Nacional." }
            immunityType: { type: string, nullable: true }
            approximateTaxes: { type: object, nullable: true, additionalProperties: true }
        additionalInfo: { type: string, nullable: true }
    Driver:
      type: object
      required: [name, cpf]
      properties:
        name: { type: string }
        cpf: { type: string, pattern: '^\d{11}$' }

    MdfeInput:
      type: object
      description: |
        Modelo canônico do MDF-e rodoviário. É **independente do layout XML**: o
        EmiteHub traduz para o layout vigente (versão de layout é escolhida pela
        plataforma). Campos marcados "(fiscal)" seguem a Nota Técnica vigente do
        MDF-e e podem ser ajustados por novas NTs.

        A plataforma preenche sozinha: número (se `number` omitido), código
        numérico, dígito verificador, chave de acesso, tipo de emissão
        (normal/contingência), versão do processo emissor e responsável técnico.
      required: [issuerId, emitterType, ufStart, ufEnd, loadingCities, vehicle, drivers, unloadings, totals]
      properties:
        issuerId: { type: string }
        environment: { $ref: "#/components/schemas/Environment" }
        modal: { type: string, enum: [road], default: road, description: "Outros modais entram no incremento I2." }
        series: { type: integer, nullable: true, description: "Padrão: série de MDF-e do emitente." }
        number: { type: integer, nullable: true, description: "Padrão: próxima numeração livre. Informar só em migração/ordem específica." }
        emitterType:
          type: string
          description: |
            Tipo de emitente (fiscal).
            - `transport_provider` — prestador de serviço de transporte.
            - `own_cargo` — transportador de carga própria.
            - `globalized_cte` — prestador que emitirá CT-e globalizado.
          enum: [transport_provider, own_cargo, globalized_cte]
        transporterType:
          type: string
          nullable: true
          description: |
            Tipo de transportador (fiscal): `etc` empresa de transporte de cargas,
            `tac` transportador autônomo de cargas, `ctc` cooperativa de transporte
            de cargas. Obrigatório quando `emitterType` ≠ `own_cargo`.
          enum: [etc, tac, ctc]
        departureAt:
          type: string
          format: date-time
          nullable: true
          description: Data/hora de início da viagem. Padrão = momento da emissão.
        ufStart: { type: string, minLength: 2, maxLength: 2, description: "UF de início do percurso." }
        ufEnd: { type: string, minLength: 2, maxLength: 2, description: "UF de fim do percurso." }
        loadingCities:
          type: array
          minItems: 1
          description: Municípios de carregamento.
          items: { $ref: "#/components/schemas/CityRef" }
        route:
          type: array
          description: UFs de percurso entre início e fim, na ordem (sem repetir início/fim).
          items: { type: string, minLength: 2, maxLength: 2 }
        vehicle: { $ref: "#/components/schemas/MdfeVehicle" }
        trailers:
          type: array
          description: Reboques (até o limite do layout).
          items: { $ref: "#/components/schemas/MdfeTrailer" }
        drivers:
          type: array
          minItems: 1
          items: { $ref: "#/components/schemas/Driver" }
        antt: { $ref: "#/components/schemas/MdfeAntt" }
        unloadings:
          type: array
          minItems: 1
          description: Municípios de descarga, cada um com os documentos fiscais entregues ali.
          items: { $ref: "#/components/schemas/Unloading" }
        cargo: { $ref: "#/components/schemas/MdfeCargo" }
        insurance:
          type: array
          items: { $ref: "#/components/schemas/MdfeInsurance" }
        seals:
          type: array
          description: Lacres do MDF-e.
          items: { type: string }
        totals: { $ref: "#/components/schemas/MdfeTotals" }
        authorizedDownloaders:
          type: array
          description: CNPJ/CPF autorizados a baixar o XML (autXML), além do emitente.
          items: { type: string, pattern: '^(\d{11}|\d{14})$' }
        additionalInfo:
          type: string
          nullable: true
          maxLength: 5000
          description: Informações complementares de interesse do contribuinte.
        externalRef:
          type: string
          nullable: true
          maxLength: 128
          description: Identificador do documento no sistema de origem (ERP/TMS). Devolvido nas consultas e webhooks.

    CityRef:
      type: object
      required: [ibgeCode, name]
      properties:
        ibgeCode: { type: string, pattern: '^\d{7}$', example: "3550308" }
        name: { type: string }

    MdfeVehicle:
      type: object
      description: Veículo de tração.
      required: [plate, plateUf, tareKg]
      properties:
        plate: { type: string, description: "Placa (padrão antigo ou Mercosul), sem hífen.", example: ABC1D23 }
        plateUf: { type: string, minLength: 2, maxLength: 2 }
        internalCode: { type: string, nullable: true, description: "Código interno do veículo no sistema do cliente." }
        renavam: { type: string, nullable: true }
        tareKg: { type: integer, minimum: 0 }
        capacityKg: { type: integer, nullable: true }
        capacityM3: { type: integer, nullable: true }
        wheelType:
          type: string
          nullable: true
          description: "Tipo de rodado (fiscal): 01 truck, 02 toco, 03 cavalo mecânico, 04 van, 05 utilitário, 06 outros."
          pattern: '^\d{2}$'
        bodyType:
          type: string
          nullable: true
          description: "Tipo de carroceria (fiscal): 00 não aplicável, 01 aberta, 02 fechada/baú, 03 graneleira, 04 porta-contêiner, 05 sider."
          pattern: '^\d{2}$'
        owner: { $ref: "#/components/schemas/VehicleOwner" }

    MdfeTrailer:
      type: object
      required: [plate, plateUf, tareKg]
      properties:
        plate: { type: string }
        plateUf: { type: string, minLength: 2, maxLength: 2 }
        internalCode: { type: string, nullable: true }
        renavam: { type: string, nullable: true }
        tareKg: { type: integer, minimum: 0 }
        capacityKg: { type: integer, nullable: true }
        capacityM3: { type: integer, nullable: true }
        bodyType: { type: string, nullable: true, pattern: '^\d{2}$' }
        owner: { $ref: "#/components/schemas/VehicleOwner" }

    VehicleOwner:
      type: object
      nullable: true
      description: Proprietário, obrigatório quando diferente do emitente (fiscal).
      required: [name]
      properties:
        cnpj: { type: string, pattern: '^\d{14}$', nullable: true }
        cpf: { type: string, pattern: '^\d{11}$', nullable: true }
        name: { type: string }
        stateRegistration: { type: string, nullable: true }
        uf: { type: string, nullable: true, minLength: 2, maxLength: 2 }
        rntrc: { type: string, nullable: true }
        ownerType:
          type: string
          nullable: true
          description: "Tipo de proprietário (fiscal): `tac_aggregate`, `tac_independent`, `other`."
          enum: [tac_aggregate, tac_independent, other]

    MdfeAntt:
      type: object
      nullable: true
      description: |
        Dados ANTT do transporte rodoviário de carga. Quais campos são
        obrigatórios depende do tipo de emitente e da NT vigente. O EmiteHub
        valida a estrutura; o cliente responde pela exigência legal.
      properties:
        rntrc: { type: string, nullable: true, description: "RNTRC do transportador." }
        ciot:
          type: array
          description: Códigos do pagamento eletrônico de frete.
          items:
            type: object
            required: [code]
            properties:
              code: { type: string, description: "Código CIOT (12 dígitos, confirmar)." }
              payerCnpj: { type: string, pattern: '^\d{14}$', nullable: true }
              payerCpf: { type: string, pattern: '^\d{11}$', nullable: true }
        tollVouchers:
          type: array
          description: Vale-pedágio.
          items:
            type: object
            required: [supplierCnpj, purchaseNumber, amount]
            properties:
              supplierCnpj: { type: string, pattern: '^\d{14}$', description: "CNPJ da empresa fornecedora." }
              payerCnpj: { type: string, pattern: '^\d{14}$', nullable: true }
              payerCpf: { type: string, pattern: '^\d{11}$', nullable: true }
              purchaseNumber: { type: string }
              amount: { type: number }
              voucherType: { type: string, nullable: true, description: "Tipo do vale-pedágio (tabela da NT, confirmar)." }
        contractors:
          type: array
          description: Contratantes do serviço de transporte.
          items:
            type: object
            required: [name]
            properties:
              cnpj: { type: string, pattern: '^\d{14}$', nullable: true }
              cpf: { type: string, pattern: '^\d{11}$', nullable: true }
              foreignId: { type: string, nullable: true }
              name: { type: string }
              contractNumber: { type: string, nullable: true }
              contractAmount: { type: number, nullable: true }
        freightPayments:
          type: array
          description: Informações de pagamento do frete.
          items: { $ref: "#/components/schemas/FreightPayment" }

    FreightPayment:
      type: object
      required: [payeeName, contractAmount]
      properties:
        payeeName: { type: string }
        payeeCnpj: { type: string, pattern: '^\d{14}$', nullable: true }
        payeeCpf: { type: string, pattern: '^\d{11}$', nullable: true }
        foreignId: { type: string, nullable: true }
        components:
          type: array
          description: Componentes do pagamento (valor do frete, pedágio, outros).
          items:
            type: object
            required: [type, amount]
            properties:
              type: { type: string, pattern: '^\d{2}$', description: "Tipo do componente (tabela da NT, confirmar)." }
              amount: { type: number }
              description: { type: string, nullable: true }
        contractAmount: { type: number }
        paymentTerms: { type: string, enum: [cash, installments], default: cash }
        advanceAmount: { type: number, nullable: true }
        installments:
          type: array
          items:
            type: object
            required: [number, dueDate, amount]
            properties:
              number: { type: integer }
              dueDate: { type: string, format: date }
              amount: { type: number }
        bank:
          type: object
          nullable: true
          description: Destino do pagamento. Informe uma das formas.
          properties:
            bankCode: { type: string, nullable: true }
            agency: { type: string, nullable: true }
            pixKey: { type: string, nullable: true }
            paymentInstitutionCnpj: { type: string, pattern: '^\d{14}$', nullable: true }

    Unloading:
      type: object
      description: Município de descarga e os documentos entregues nele.
      required: [city, documents]
      properties:
        city: { $ref: "#/components/schemas/CityRef" }
        documents:
          type: array
          minItems: 1
          items: { $ref: "#/components/schemas/LinkedDocument" }

    LinkedDocument:
      type: object
      required: [type, accessKey]
      properties:
        type: { type: string, enum: [nfe, cte, mdfe_transport], description: "NF-e, CT-e ou MDF-e de transporte." }
        accessKey: { type: string, pattern: '^\d{44}$' }
        fallbackBarcode: { type: string, nullable: true, description: "Segundo código de barras (contingência FS-DA/offline), se houver." }
        redelivery: { type: boolean, nullable: true, description: "Indica reentrega." }
        transportUnits:
          type: array
          nullable: true
          description: Unidades de transporte/carga (contêiner, ULD, etc.) deste documento.
          items:
            type: object
            required: [type, id]
            properties:
              type: { type: string, description: "Tipo da unidade (tabela da NT, confirmar)." }
              id: { type: string }
              seals: { type: array, items: { type: string } }
        dangerousGoods:
          type: array
          nullable: true
          description: Produtos perigosos deste documento (ONU, classe, grupo, quantidade).
          items:
            type: object
            required: [unNumber]
            properties:
              unNumber: { type: string, description: "Número ONU." }
              properName: { type: string, nullable: true }
              riskClass: { type: string, nullable: true }
              packingGroup: { type: string, nullable: true }
              quantity: { type: string, nullable: true }

    MdfeCargo:
      type: object
      nullable: true
      description: Produto predominante e, em carga lotação, os locais de carga/descarga.
      properties:
        cargoType:
          type: string
          pattern: '^\d{2}$'
          description: "Tipo de carga (fiscal): 01 granel sólido, 02 granel líquido, 03 frigorificada, 04 conteinerizada, 05 carga geral, 06 neogranel, 07–11 variantes de carga perigosa (tabela da NT, confirmar)."
        productName: { type: string, description: "Descrição do produto predominante." }
        ean: { type: string, nullable: true }
        ncm: { type: string, pattern: '^\d{2,8}$', nullable: true }
        fullLoad:
          type: object
          nullable: true
          description: Carga lotação (um só carregamento e uma só descarga).
          properties:
            loadingPlace: { $ref: "#/components/schemas/Place" }
            unloadingPlace: { $ref: "#/components/schemas/Place" }

    Place:
      type: object
      description: Local por CEP ou por coordenadas.
      properties:
        zipCode: { type: string, pattern: '^\d{8}$', nullable: true }
        latitude: { type: number, nullable: true }
        longitude: { type: number, nullable: true }

    MdfeInsurance:
      type: object
      required: [responsible, insurerCnpj, policyNumber]
      properties:
        responsible: { type: string, enum: [issuer, contractor], description: "Responsável pelo seguro." }
        responsibleCnpj: { type: string, pattern: '^\d{14}$', nullable: true }
        responsibleCpf: { type: string, pattern: '^\d{11}$', nullable: true }
        insurerName: { type: string, nullable: true }
        insurerCnpj: { type: string, pattern: '^\d{14}$' }
        policyNumber: { type: string }
        endorsements:
          type: array
          description: Números de averbação.
          items: { type: string }

    MdfeTotals:
      type: object
      required: [cargoValue, cargoWeight, weightUnit]
      properties:
        cargoValue: { type: number, minimum: 0, description: "Valor total da carga em R$." }
        cargoWeight: { type: number, minimum: 0, description: "Peso bruto total." }
        weightUnit: { type: string, enum: [kg, ton] }
        nfeCount: { type: integer, nullable: true, description: "Opcional. Se informado, deve bater com a quantidade de NF-e em `unloadings`." }
        cteCount: { type: integer, nullable: true, description: "Opcional. Idem para CT-e." }

    FiscalDocument:
      type: object
      required: [id, kind, status, issuerId, environment, createdAt, updatedAt]
      properties:
        id: { type: string, description: "Identificador opaco. Chave de upsert do cliente." }
        kind: { type: string, enum: [nfe, mdfe] }
        status: { $ref: "#/components/schemas/DocStatus" }
        issuerId: { type: string }
        environment: { $ref: "#/components/schemas/Environment" }
        series: { type: integer, nullable: true }
        number: { type: integer, nullable: true }
        accessKey: { type: string, nullable: true, description: "Chave de 44 dígitos." }
        protocol: { type: string, nullable: true }
        authorizedAt: { type: string, format: date-time, nullable: true }
        rejection:
          type: object
          nullable: true
          properties:
            sefazCode: { type: string }
            message: { type: string }
            hint: { type: string, nullable: true, description: "Como corrigir." }
        events:
          type: array
          items:
            type: object
            properties:
              type: { type: string, enum: [cancellation, correction, closure, driver_inclusion, document_inclusion] }
              status: { type: string }
              protocol: { type: string, nullable: true }
              occurredAt: { type: string, format: date-time }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    WebhookEvent:
      type: string
      enum:
        - nfe.authorized
        - nfe.rejected
        - nfe.cancelled
        - mdfe.authorized
        - mdfe.rejected
        - mdfe.closed
        - mdfe.cancelled
        - certificate.expiring

    Webhook:
      type: object
      properties:
        id: { type: string }
        url: { type: string, format: uri }
        events: { type: array, items: { $ref: "#/components/schemas/WebhookEvent" } }
        secret: { type: string, description: "Só na criação." }
        createdAt: { type: string, format: date-time }

    WebhookPayload:
      type: object
      required: [id, event, createdAt, data]
      properties:
        id: { type: string, description: "Id do evento; use para deduplicar." }
        event: { $ref: "#/components/schemas/WebhookEvent" }
        createdAt: { type: string, format: date-time }
        data: { $ref: "#/components/schemas/FiscalDocument" }

    WebhookDelivery:
      type: object
      properties:
        id: { type: string }
        webhookId: { type: string }
        event: { type: string }
        status: { type: string, enum: [pending, delivered, failed] }
        attempts: { type: integer }
        lastHttpStatus: { type: integer, nullable: true }
        nextAttemptAt: { type: string, format: date-time, nullable: true }

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Código estável. Decida por ele e pelo HTTP, nunca pelo texto.
              example: VALIDATION_ERROR
            message: { type: string }
            sefazCode: { type: string, nullable: true }
            details:
              type: array
              items:
                type: object
                properties:
                  field: { type: string }
                  issue: { type: string }

  responses:
    Accepted:
      description: Aceito para processamento. Resultado por webhook e `GET`.
      headers:
        RateLimit-Limit: { $ref: "#/components/headers/RateLimit-Limit" }
        RateLimit-Remaining: { $ref: "#/components/headers/RateLimit-Remaining" }
        RateLimit-Reset: { $ref: "#/components/headers/RateLimit-Reset" }
      content:
        application/json:
          schema:
            type: object
            required: [id, status]
            properties:
              id: { type: string }
              status: { type: string, example: processing }
    FileUrl:
      description: URL assinada, válida por cerca de 1 hora.
      content:
        application/json:
          schema:
            type: object
            required: [url, expiresAt]
            properties:
              url: { type: string, format: uri }
              expiresAt: { type: string, format: date-time }
    BadRequest:
      description: Requisição malformada ou parâmetro inválido. Repetir não resolve.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unauthorized:
      description: Chave ausente, inválida, expirada ou revogada.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    NotFound:
      description: Recurso não encontrado.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Conflict:
      description: "Conflito (ex.: emitente já cadastrado)."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    Unprocessable:
      description: "Dados válidos em formato, mas rejeitados por regra fiscal/de estado (ex.: cancelar nota já cancelada)."
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    TooManyRequests:
      description: Limite excedido. Aguarde `Retry-After` segundos.
      headers:
        Retry-After: { schema: { type: integer } }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
