openapi: 3.1.0
info:
  title: Emitfy REST API
  version: 0.2.1
  description: |
    API REST Emitfy (camelCase inglês). Modelo Conta × Assinatura:
    - Keys e webhooks na assinatura (`/webhooks`, sem companyId na URL)
    - Emissão e catálogo sob `/companies/{companyId}/...`
    Envelope: `{ "success": true, "data": ... }` / `{ "success": false, "error": { "code", "message", "details" } }`.
    Validação Zod → HTTP 422 + `VALIDATION_ERROR`.
    Fonte para agentes: https://api.emitfy.com/llms-full.txt
servers:
  - url: https://api.emitfy.com/v1
security:
  - ApiKeyAuth: []
    ApiSecretAuth: []
tags:
  - name: webhooks
  - name: companies
  - name: customers
  - name: products
  - name: sales
  - name: nfse
  - name: nfe
  - name: nfce
  - name: cte
  - name: invoices
  - name: received-nfes
  - name: credentials
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
    ApiSecretAuth:
      type: apiKey
      in: header
      name: X-Api-Secret
  parameters:
    CompanyId:
      name: companyId
      in: path
      required: true
      schema: { type: string, format: uuid }
      description: ID da empresa (CNPJ operacional) no path
    ResourceId:
      name: id
      in: path
      required: true
      schema: { type: string }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string }
    Page:
      name: page
      in: query
      schema: { type: integer, minimum: 1, default: 1 }
    PageSize:
      name: pageSize
      in: query
      schema: { type: integer, minimum: 1, maximum: 100 }
  schemas:
    ErrorBody:
      type: object
      required: [success, error]
      properties:
        success: { type: boolean, const: false }
        error:
          type: object
          required: [code, message]
          properties:
            code: { type: string }
            message: { type: string }
            details:
              nullable: true
              additionalProperties: true
    SuccessEnvelope:
      type: object
      required: [success]
      properties:
        success: { type: boolean, const: true }
        data:
          description: Payload da operação
          additionalProperties: true
    WebhookCreate:
      type: object
      required: [url, events]
      properties:
        name: { type: string }
        url: { type: string, format: uri }
        headers:
          type: array
          items:
            type: object
            required: [key, value]
            properties:
              key: { type: string }
              value: { type: string }
        events:
          type: object
          properties:
            invoice:
              type: array
              items: { type: string }
            cte:
              type: array
              items: { type: string }
paths:
  /webhooks:
    get:
      summary: List subscription webhooks
      operationId: webhooksList
      tags: [webhooks]
      responses:
        "200":
          description: Webhooks da assinatura (todas as empresas)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessEnvelope' }
    post:
      summary: Create subscription webhook
      operationId: webhooksCreate
      tags: [webhooks]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookCreate' }
            examples:
              minimal:
                value:
                  name: Meu ERP
                  url: https://seu-sistema.com/webhooks/emitfy
                  events:
                    invoice: [nfse.authorized, nfe.authorized]
                    cte: []
      responses:
        "200":
          description: Webhook criado
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessEnvelope' }

  /webhooks/{id}:
    parameters:
      - $ref: '#/components/parameters/ResourceId'
    put:
      summary: Update webhook
      operationId: webhooksUpdate
      tags: [webhooks]
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookCreate' }
      responses:
        "200":
          description: Webhook atualizado
    delete:
      summary: Delete webhook
      operationId: webhooksDelete
      tags: [webhooks]
      responses:
        "200":
          description: Webhook removido

  /webhooks/{id}/active:
    parameters:
      - $ref: '#/components/parameters/ResourceId'
    patch:
      summary: Activate or deactivate webhook
      operationId: webhooksSetActive
      tags: [webhooks]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [active]
              properties:
                active: { type: boolean }
      responses:
        "200":
          description: Estado active atualizado

  /user/api-credentials:
    get:
      summary: Get API credential status
      operationId: apiCredentialsStatus
      tags: [credentials]
      responses:
        "200":
          description: Status das credenciais (secret nunca retornado aqui)

  /companies:
    get:
      summary: List companies in the subscription
      operationId: companiesList
      tags: [companies]
      responses:
        "200":
          description: Lista de empresas (CNPJs) da assinatura
    post:
      summary: Create company
      operationId: companiesCreate
      tags: [companies]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Empresa criada

  /companies/{companyId}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      summary: Get company
      operationId: companiesGet
      tags: [companies]
      responses:
        "200":
          description: Empresa
        "404":
          description: Não encontrada
    put:
      summary: Update company
      operationId: companiesUpdate
      tags: [companies]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Empresa atualizada
    delete:
      summary: Delete or detach company
      operationId: companiesDelete
      tags: [companies]
      responses:
        "200":
          description: Empresa removida/desvinculada

  /companies/{companyId}/certificate:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      summary: Get certificate status
      operationId: companyCertificateGet
      tags: [companies]
      responses:
        "200":
          description: Status do certificado A1
    post:
      summary: Upload A1 certificate
      operationId: companyCertificateUpload
      tags: [companies]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Certificado enviado
    delete:
      summary: Remove certificate
      operationId: companyCertificateDelete
      tags: [companies]
      responses:
        "200":
          description: Certificado removido

  /companies/{companyId}/status:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      summary: Company fiscal status
      operationId: companyStatus
      tags: [companies]
      responses:
        "200":
          description: Status fiscal / módulos

  /companies/{companyId}/environment:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    patch:
      summary: Switch homologation / production
      operationId: companyEnvironment
      tags: [companies]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Ambiente atualizado

  /companies/{companyId}/customers:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      summary: List customers
      operationId: customersList
      tags: [customers]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
        - name: search
          in: query
          schema: { type: string }
      responses:
        "200":
          description: Lista paginada
    post:
      summary: Create or upsert customer
      operationId: customersCreate
      tags: [customers]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Cliente criado/atualizado

  /companies/{companyId}/customers/{id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/ResourceId'
    get:
      summary: Get customer
      operationId: customersGet
      tags: [customers]
      responses:
        "200":
          description: Cliente
        "404":
          description: Não encontrado
    put:
      summary: Update customer (overwrite)
      operationId: customersUpdate
      tags: [customers]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Cliente atualizado
    delete:
      summary: Delete customer
      operationId: customersDelete
      tags: [customers]
      responses:
        "200":
          description: Cliente removido

  /companies/{companyId}/products:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      summary: List products
      operationId: productsList
      tags: [products]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
        - name: search
          in: query
          schema: { type: string }
        - name: type
          in: query
          schema: { type: string }
      responses:
        "200":
          description: Lista paginada
    post:
      summary: Create product
      operationId: productsCreate
      tags: [products]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Produto criado

  /companies/{companyId}/products/{id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/ResourceId'
    get:
      summary: Get product
      operationId: productsGet
      tags: [products]
      responses:
        "200":
          description: Produto
    put:
      summary: Update product
      operationId: productsUpdate
      tags: [products]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Produto atualizado
    delete:
      summary: Delete product
      operationId: productsDelete
      tags: [products]
      responses:
        "200":
          description: Produto removido

  /companies/{companyId}/sales:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      summary: List sales
      operationId: salesList
      tags: [sales]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        "200":
          description: Lista paginada
    post:
      summary: Create sale (+ draft invoice pipeline)
      operationId: salesCreate
      tags: [sales]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Fonte-canônica Zod SaleCreateRequestBodySchema
              additionalProperties: true
            examples:
              minimal:
                summary: Corpo mínimo
                value:
                  customer: "00000000-0000-4000-8000-000000000001"
                  saleDate: "2026-01-15T14:30:00.000Z"
                  products:
                    - sku: "MEU_SKU_SERVICO"
                      price: 100
                      quantity: 1
              full:
                summary: Corpo completo (típico)
                value:
                  customer:
                    name: "João da Silva"
                    email: "joao@email.com"
                    document: "12345678901"
                    phone: "(11) 98888-7777"
                    address:
                      zipcode: "01310100"
                      street: "Av. Paulista"
                      number: "1000"
                      district: "Bela Vista"
                      city: "São Paulo"
                      state: "SP"
                      complement: "Sala 1"
                  paymentMethod: "pix"
                  saleDate: "2026-01-15T14:30:00.000Z"
                  emitWhen: "after_guarantee"
                  guaranteeDate: "2026-02-15T00:00:00.000Z"
                  sendEmail: false
                  externalId: "pedido-checkout-8899"
                  discount: 0
                  shipping: 9.9
                  total: 109.9
                  products:
                    - id: "01ARZ3NDEKTSV4RRFFQ69G5FAV"
                      name: "Assinatura anual — plano ouro"
                      type: "service"
                      category: "saas"
                      price: 100
                      quantity: 1
                  nfse:
                    discrimination: "Descrição discriminada do serviço para a prefeitura."
                    competenceDate: "2026-01-15T03:00:00.000Z"
                    serviceLocation: "1"
                    cityIbge: "3550308"
      responses:
        "200":
          description: Venda criada
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessEnvelope' }

  /companies/{companyId}/sales/{id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/ResourceId'
    get:
      summary: Get sale
      operationId: salesGet
      tags: [sales]
      responses:
        "200":
          description: Venda
    put:
      summary: Update sale
      operationId: salesUpdate
      tags: [sales]
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Venda atualizada
    delete:
      summary: Delete sale
      operationId: salesDelete
      tags: [sales]
      responses:
        "200":
          description: Venda removida

  /companies/{companyId}/sales/{id}/emit-invoice:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/ResourceId'
    post:
      summary: Emit invoice for sale
      operationId: salesEmitInvoice
      tags: [sales]
      responses:
        "200":
          description: Emissão enfileirada

  /companies/{companyId}/sales/{id}/cancel-invoice:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/ResourceId'
    post:
      summary: Cancel invoice for sale
      operationId: salesCancelInvoice
      tags: [sales]
      responses:
        "200":
          description: Cancelamento enfileirado

  /companies/{companyId}/nfse:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      summary: List NFS-e
      operationId: nfseList
      tags: [nfse]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
        - name: status
          in: query
          schema:
            type: string
            enum: [processing, authorized, rejected, canceled]
      responses:
        "200":
          description: Lista paginada
    post:
      summary: Emit NFS-e (shortcut)
      operationId: nfseCreate
      tags: [nfse]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: NFS-e enfileirada

  /companies/{companyId}/nfse/{id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/ResourceId'
    get:
      summary: Get NFS-e
      operationId: nfseGet
      tags: [nfse]
      responses:
        "200":
          description: NFS-e
        "404":
          description: Não encontrada
    delete:
      summary: Cancel NFS-e
      operationId: nfseCancel
      tags: [nfse]
      responses:
        "200":
          description: Cancelamento enfileirado
        "422":
          description: Status não permite cancelamento

  /companies/{companyId}/nfe:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      summary: List NF-e
      operationId: nfeList
      tags: [nfe]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        "200":
          description: Lista paginada
    post:
      summary: Emit NF-e (shortcut)
      operationId: nfeCreate
      tags: [nfe]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: NF-e enfileirada

  /companies/{companyId}/nfe/{id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/ResourceId'
    get:
      summary: Get NF-e
      operationId: nfeGet
      tags: [nfe]
      responses:
        "200":
          description: NF-e
    delete:
      summary: Cancel NF-e
      operationId: nfeCancel
      tags: [nfe]
      responses:
        "200":
          description: Cancelamento enfileirado
        "422":
          description: Status não permite cancelamento

  /companies/{companyId}/nfce:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      summary: List NFC-e
      operationId: nfceList
      tags: [nfce]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, processing, authorized, rejected, canceled, error]
      responses:
        "200":
          description: Lista paginada
    post:
      summary: Emit NFC-e
      operationId: nfceCreate
      tags: [nfce]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [paymentMethod, items]
              properties:
                paymentMethod:
                  type: string
                  enum: [credit_card, debit_card, pix, bank_transfer, cash, bank_slip, other]
                items:
                  type: array
                  minItems: 1
                  items:
                    type: object
                    required: [description, quantity, unitValue]
                    additionalProperties: true
                recipient:
                  type: object
                  additionalProperties: true
                externalId: { type: string }
                issueMode:
                  type: string
                  enum: [normal, offlineContingency]
                  default: normal
                contingency:
                  type: object
                  properties:
                    startedAt: { type: string, format: date-time }
                    reason: { type: string, minLength: 15 }
                accessKey: { type: string }
                transmit:
                  type: string
                  enum: [now, later]
                  default: now
              additionalProperties: true
            examples:
              withRecipient:
                value:
                  paymentMethod: pix
                  items:
                    - description: Café espresso
                      ncm: "09012100"
                      quantity: 2
                      unitValue: 8.5
                      unit: UN
                  recipient:
                    taxId: "12345678900"
                    name: Maria Souza
                  externalId: pedido-nfce-100
              offlineContingency:
                value:
                  issueMode: offlineContingency
                  transmit: later
                  accessKey: "35260714200166000187650010000012349123456781"
                  number: 12345
                  series: 1
                  issuedAt: "2026-07-12T18:40:00-03:00"
                  contingency:
                    startedAt: "2026-07-12T18:39:55-03:00"
                    reason: SEFAZ indisponivel ou sem conexao
                  paymentMethod: pix
                  items:
                    - description: Café espresso
                      quantity: 2
                      unitValue: 8.5
      responses:
        "200":
          description: NFC-e existente (idempotência)
        "202":
          description: NFC-e criada

  /companies/{companyId}/nfce/{id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/ResourceId'
    get:
      summary: Get NFC-e
      operationId: nfceGet
      tags: [nfce]
      responses:
        "200":
          description: NFC-e
        "404":
          description: Não encontrada
    delete:
      summary: Cancel NFC-e
      operationId: nfceCancel
      tags: [nfce]
      responses:
        "202":
          description: Cancelamento enfileirado
        "422":
          description: Status não permite cancelamento

  /companies/{companyId}/nfce/{id}/transmit:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/ResourceId'
    post:
      summary: Transmit NFC-e to SEFAZ
      operationId: nfceTransmit
      tags: [nfce]
      responses:
        "200":
          description: Já autorizada ou já na fila
        "202":
          description: Transmissão enfileirada
        "409":
          description: Status não permite transmitir
        "422":
          description: Validação de contingência falhou

  /companies/{companyId}/cte:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      summary: List CT-e
      operationId: cteList
      tags: [cte]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, queued, processing, authorized, rejected, cancelled]
      responses:
        "200":
          description: Lista paginada
    post:
      summary: Emit CT-e (modelo 57)
      operationId: cteCreate
      tags: [cte]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - cfop
                - nature
                - shipper
                - recipient
                - municipalityStartIbge
                - municipalityEndIbge
                - cargoValue
                - cargoProduct
                - cargoQuantities
                - serviceAmount
                - icms
              additionalProperties: true
      responses:
        "200":
          description: CT-e enfileirado

  /companies/{companyId}/cte/{id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/ResourceId'
    get:
      summary: Get CT-e
      operationId: cteGet
      tags: [cte]
      responses:
        "200":
          description: CT-e
        "404":
          description: Não encontrado
    delete:
      summary: Cancel CT-e
      operationId: cteCancel
      tags: [cte]
      responses:
        "200":
          description: Cancelamento enfileirado
        "422":
          description: Status não permite cancelamento

  /companies/{companyId}/cte-os:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    post:
      summary: Emit CT-e OS (modelo 67)
      operationId: cteOsCreate
      tags: [cte]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cfop, nature, serviceDescription, serviceAmount, icms]
              additionalProperties: true
      responses:
        "200":
          description: CT-e OS enfileirado
        "422":
          description: Validação ou módulo inativo

  /companies/{companyId}/invoices:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      summary: List invoices
      operationId: invoicesList
      tags: [invoices]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PageSize'
      responses:
        "200":
          description: Lista paginada

  /companies/{companyId}/invoices/{id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/ResourceId'
    get:
      summary: Get invoice
      operationId: invoicesGet
      tags: [invoices]
      responses:
        "200":
          description: Nota unificada

  /companies/{companyId}/received-nfes:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    get:
      summary: List received NF-e
      operationId: receivedNfesList
      tags: [received-nfes]
      parameters:
        - $ref: '#/components/parameters/Page'
        - name: limit
          in: query
          schema: { type: integer }
        - name: search
          in: query
          schema: { type: string }
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, science, confirmed, unknown, notPerformed]
      responses:
        "200":
          description: Lista paginada de NF-e destinadas ao CNPJ

  /companies/{companyId}/received-nfes/sync:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
    post:
      summary: Enqueue DistDFe sync
      operationId: receivedNfesSync
      tags: [received-nfes]
      responses:
        "202":
          description: Sync aceito
        "422":
          description: Certificado ausente ou NF-e inativa
        "503":
          description: Falha ao enfileirar

  /companies/{companyId}/received-nfes/{id}:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/ResourceId'
    get:
      summary: Get received NF-e
      operationId: receivedNfesGet
      tags: [received-nfes]
      responses:
        "200":
          description: NF-e recebida

  /companies/{companyId}/received-nfes/{id}/xml:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/ResourceId'
    get:
      summary: Get received NF-e XML
      operationId: receivedNfesXml
      tags: [received-nfes]
      responses:
        "200":
          description: "{ xml, accessKey, xmlUrl }"

  /companies/{companyId}/received-nfes/{id}/manifest:
    parameters:
      - $ref: '#/components/parameters/CompanyId'
      - $ref: '#/components/parameters/ResourceId'
    post:
      summary: Manifest recipient event
      operationId: receivedNfesManifest
      tags: [received-nfes]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type]
              properties:
                type:
                  type: string
                  enum: [confirmed, unknown, notPerformed]
                justification:
                  type: string
                  description: Obrigatório para notPerformed (15–255 chars)
            examples:
              confirm:
                value: { type: confirmed }
              notPerformed:
                value:
                  type: notPerformed
                  justification: Mercadoria devolvida ao remetente
      responses:
        "200":
          description: NF-e recebida atualizada após evento SEFAZ
