openapi: 3.0.0
paths:
  /v1/health:
    get:
      description: >-
        Sin autenticación. Úsala para distinguir "la API está caída" de "mi
        credencial es incorrecta" antes de escalar.
      operationId: ExternalInvoicingController_health
      parameters: []
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponseDto'
      summary: Comprobación de disponibilidad (liveness)
      tags:
        - Facturación
  /v1/clients:
    post:
      description: >-
        Devuelve el id del cliente que pasas como `clientId` al crear facturas.
        Un cliente solo es requerido para ventas exoneradas; las ventas
        ordinarias pueden omitirlo.
      operationId: ExternalInvoicingController_createClient
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateExternalClientDto'
      responses:
        '201':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalClientResponseDto'
        '400':
          description: Fallo de validación, p. ej. INVALID_RTN_FORMAT.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
        '401':
          description: MISSING_API_KEY or INVALID_API_KEY.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
        '403':
          description: INTEGRATION_NOT_ENABLED — la empresa no ha habilitado la API.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
        '409':
          description: Ya existe un cliente con este código o RTN.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
      security:
        - ApiKey: []
      summary: Crear un cliente
      tags:
        - Facturación
    get:
      description: >-
        Devuelve los clientes de tu empresa, ordenados por nombre. Úsala para
        recuperar el `id` de un cliente ya creado sin tener que guardarlo tú, p.
        ej. antes de emitir una venta exonerada. La paginación es opcional: sin
        `page`/`limit` se devuelven todos.
      operationId: ExternalInvoicingController_listClients
      parameters:
        - name: page
          required: false
          in: query
          description: Página, empezando en 1. Requiere también `limit`.
          schema:
            type: number
        - name: limit
          required: false
          in: query
          description: Clientes por página. Requiere también `page`.
          schema:
            type: number
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalClientListResponseDto'
        '401':
          description: MISSING_API_KEY or INVALID_API_KEY.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
        '403':
          description: INTEGRATION_NOT_ENABLED — la empresa no ha habilitado la API.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
      security:
        - ApiKey: []
      summary: Listar tus clientes
      tags:
        - Facturación
  /v1/invoices:
    post:
      description: >-
        Crea una factura legal contra el rango CAI de la empresa.


        SEGURIDAD DE REINTENTO: genera un Idempotency-Key por factura lógica,
        persístelo ANTES de la primera llamada y envía la misma key y el mismo
        body en cada reintento. En un timeout o 5xx, reintentar con esa key es
        seguro: obtienes la factura original de vuelta con `replayed: true` en
        lugar de una segunda factura. Reutilizar una key con un body DIFERENTE
        se rechaza con 409 — eso es un error del lado del llamador, no un
        reintento.
      operationId: ExternalInvoicingController_createInvoice
      parameters:
        - name: idempotency-key
          required: true
          in: header
          schema:
            type: string
        - name: Idempotency-Key
          in: header
          description: >-
            Único por factura lógica. Tu propio id de orden sirve; no tiene que
            ser un UUID. Las keys se conservan durante unas 24 h.
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateExternalInvoiceDto'
      responses:
        '201':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalInvoiceResponseDto'
        '400':
          description: >-
            MISSING_IDEMPOTENCY_KEY, EXTERNAL_INVOICE_NO_ITEMS,
            EXTERNAL_INVOICE_CLIENT_NOT_FOUND,
            EXTERNAL_INVOICE_EXONERATION_REQUIRES_RTN, o un fallo de validación
            del body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
        '401':
          description: MISSING_API_KEY or INVALID_API_KEY.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
        '403':
          description: INTEGRATION_NOT_ENABLED.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
        '409':
          description: >-
            IDEMPOTENCY_KEY_REQUEST_MISMATCH (misma key, body diferente), o la
            integración de la empresa está mal configurada
            (INTEGRATION_MISSING_*).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
        '422':
          description: >-
            EXTERNAL_INVOICE_CREDIT_NOT_SUPPORTED — `isCredit: true` está
            reservado para v2 y hoy se rechaza.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
      security:
        - ApiKey: []
      summary: Emitir una factura
      tags:
        - Facturación
  /v1/invoices/preview:
    post:
      description: >-
        Calcula y renderiza el documento con TU configuración real — tus
        productos, tu lista de precios, tus tasas de ISV, tu membrete — usando
        exactamente el mismo motor y la misma plantilla que una factura real, y
        luego lo descarta.


        No consume CAI, no avanza el correlativo y no escribe nada. Sirve para
        validar tu integración y ver el formato impreso antes de emitir la
        primera factura de verdad.


        El PDF viene marcado SIN VALIDEZ FISCAL y el CAI aparece enmascarado. No
        lo entregues a un cliente final: no sustituye una factura.


        Mismo body que POST /v1/invoices, pero sin header Idempotency-Key — no
        hay nada que reintentar de forma idempotente porque no se emite nada.
      operationId: ExternalInvoicingController_previewInvoice
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateExternalInvoiceDto'
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalInvoicePreviewResponseDto'
        '400':
          description: >-
            EXTERNAL_INVOICE_NO_ITEMS, EXTERNAL_INVOICE_CLIENT_NOT_FOUND,
            EXTERNAL_INVOICE_EXONERATION_REQUIRES_RTN, o un fallo de validación
            del body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
        '401':
          description: MISSING_API_KEY or INVALID_API_KEY.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
        '403':
          description: INTEGRATION_NOT_ENABLED.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
        '409':
          description: >-
            INTEGRATION_MISSING_EMISSION_POINT / _CREATOR_USER / _PRICE_TYPE. A
            diferencia de POST /v1/invoices, la previsualización NO exige un
            terminal de pago configurado.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
      security:
        - ApiKey: []
      summary: Previsualizar una factura (sin emitirla)
      tags:
        - Facturación
  /v1/invoices/{id}:
    get:
      description: >-
        Limitada a tu propia empresa. Un id fuera de ese alcance responde el
        mismo 404 que un id inexistente.
      operationId: ExternalInvoicingController_getInvoice
      parameters:
        - name: id
          required: true
          in: path
          schema:
            type: number
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalInvoiceResponseDto'
        '401':
          description: MISSING_API_KEY or INVALID_API_KEY.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
        '404':
          description: EXTERNAL_INVOICE_NOT_FOUND.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
      security:
        - ApiKey: []
      summary: Leer una factura
      tags:
        - Facturación
  /v1/invoices/{id}/pdf:
    get:
      description: >-
        Devuelve el mismo documento A4 que imprime Contadito, con CAI, rango
        autorizado y fecha límite de emisión. Limitada a tu propia empresa: un
        id fuera de ese alcance responde el mismo 404 que un id inexistente.
      operationId: ExternalInvoicingController_getInvoicePdf
      parameters:
        - name: id
          required: true
          in: path
          schema:
            type: number
      responses:
        '200':
          description: El PDF de la factura.
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        '401':
          description: MISSING_API_KEY or INVALID_API_KEY.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
        '404':
          description: EXTERNAL_INVOICE_NOT_FOUND.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExternalApiErrorResponseDto'
      security:
        - ApiKey: []
      summary: Descargar el PDF de una factura
      tags:
        - Facturación
info:
  title: Contadito Invoicing API
  description: >-
    Emite facturas de venta legalmente válidas en Honduras desde tu propia
    aplicación.


    **Autenticación.** Envía tu API key como `Authorization: Bearer <key>`. Las
    keys se ven como `ctd_sk_…`, se emiten desde la app de Contadito y se
    muestran una sola vez — guarda la tuya antes de cerrar ese diálogo. La key
    identifica a tu empresa; no hay que enviarla en ningún campo de la solicitud.


    **Antes de empezar.** El personal de Contadito debe (1) habilitar la
    integración para tu empresa y (2) mapear tus ids de producto a los productos
    de Contadito. Hasta entonces las solicitudes fallan con
    `INTEGRATION_NOT_ENABLED` o un error de producto sin mapear.


    **Errores** siempre devuelven `code` y `message` como campos JSON separados.
    Usa `code` para la lógica; trata `message` como texto legible cuya redacción
    puede cambiar.


    **Reintentos.** `POST /v1/invoices` requiere un `Idempotency-Key`. Lee ese
    endpoint antes de escribir tu lógica de reintento — la numeración de
    facturas está regulada y las facturas duplicadas son un problema de
    cumplimiento, no solo de datos.
  version: '1.0'
  contact: {}
tags: []
servers: []
components:
  securitySchemes:
    ApiKey:
      scheme: bearer
      bearerFormat: JWT
      type: http
      description: Tu API key de Contadito (`ctd_sk_…`).
  schemas:
    HealthResponseDto:
      type: object
      properties:
        status:
          type: string
          example: ok
      required:
        - status
    CreateExternalClientDto:
      type: object
      properties:
        clientName:
          type: string
          example: Servicios Digitales S. de R.L.
          maxLength: 255
        email:
          type: string
          example: facturacion@cliente.hn
        legalId:
          type: string
          description: >-
            RTN. Se valida como exactamente 14 dígitos cuando está presente.
            Requerido si tienes intención de emitir facturas exoneradas para
            este cliente.
          example: '08011999123456'
          maxLength: 32
        phone:
          type: string
          example: +504 9999-9999
          maxLength: 32
        address:
          type: string
          example: Tegucigalpa, Honduras
          maxLength: 255
      required:
        - clientName
        - email
    ExternalClientResponseDto:
      type: object
      properties:
        id:
          type: number
          description: Usa este id como `clientId` en POST /v1/invoices.
        clientName:
          type: string
        email:
          type: string
        legalId:
          type: string
          nullable: true
        phone:
          type: string
          nullable: true
        address:
          type: string
          nullable: true
        createdAt:
          format: date-time
          type: string
      required:
        - id
        - clientName
        - email
        - createdAt
    ExternalApiErrorResponseDto:
      type: object
      properties:
        code:
          type: string
          description: >-
            Código estable y legible por máquina. Úsalo para la lógica, nunca
            `message` — la redacción de `message` puede cambiar.
          example: INTEGRATION_MISSING_PRICE_TYPE
        message:
          type: string
          description: Explicación legible por humanos. No es un contrato estable.
          example: No está configurado un tipo de precio por defecto.
      required:
        - code
        - message
    ExternalClientListResponseDto:
      type: object
      properties:
        clients:
          type: array
          items:
            $ref: '#/components/schemas/ExternalClientResponseDto'
        count:
          type: number
          description: Total de clientes de la empresa, no el tamaño de esta página.
      required:
        - clients
        - count
    CreateExternalInvoiceItemDto:
      type: object
      properties:
        externalProductId:
          type: string
          description: >-
            Tu propio identificador del producto. El personal de Contadito lo
            mapea a un producto interno una sola vez; en v1 no hay endpoint de
            búsqueda de catálogo. Un id sin mapear es rechazado.
          example: SKU-WEB-HOSTING-01
        quantity:
          type: number
          description: Debe ser mayor que cero.
          example: 2
          minimum: 0
          exclusiveMinimum: true
      required:
        - externalProductId
        - quantity
    CreateExternalInvoiceDto:
      type: object
      properties:
        clientId:
          type: number
          description: >-
            Id del cliente en Contadito, tal como lo devuelve POST /v1/clients.
            Omitir para una venta sin cliente registrado. Requerido cuando la
            venta es exonerada.
          example: 1042
        invoiceDate:
          type: string
          description: >-
            Fecha ISO 8601. Por defecto es la fecha y hora actuales. La fecha
            retroactiva está fuera del alcance de v1.
          example: '2026-08-10T15:04:05.000Z'
        purchaseOrderNumber:
          type: string
          description: Tu propia referencia de orden de compra, registrada en la factura.
          example: PO-2026-0917
        isCredit:
          type: boolean
          description: >-
            RESERVADO PARA v2 — enviar `true` es rechazado con
            EXTERNAL_INVOICE_CREDIT_NOT_SUPPORTED. v1 solo admite ventas al
            contado. El campo existe para poder agregar facturas de crédito en
            el futuro sin un cambio incompatible.
          example: false
          default: false
        items:
          description: Se requiere al menos una línea.
          type: array
          items:
            $ref: '#/components/schemas/CreateExternalInvoiceItemDto'
      required:
        - items
    ExternalInvoiceTaxBreakdownResponseDto:
      type: object
      properties:
        rate:
          type: number
          description: Tasa de ISV en porcentaje.
          example: 15
        taxAmount:
          type: number
          description: Impuesto cobrado a esta tasa.
        taxedAmount:
          type: number
          description: Base gravable a la que se aplicó esta tasa.
      required:
        - rate
        - taxAmount
        - taxedAmount
    ExternalInvoiceLineResponseDto:
      type: object
      properties:
        externalProductId:
          type: string
          nullable: true
        quantity:
          type: number
        unitPrice:
          type: number
          description: Precio unitario facturado, sin impuestos.
        importe:
          type: number
          description: unitPrice x quantity, antes del descuento.
        discountAmount:
          type: number
        subTotal:
          type: number
          description: Total de la línea tras el descuento, antes de impuestos.
        exemptAmount:
          type: number
          nullable: true
          description: >-
            Siempre null en lecturas — no se persiste una columna de exento por
            línea.
      required:
        - quantity
        - unitPrice
        - importe
        - discountAmount
        - subTotal
    ExternalInvoiceResponseDto:
      type: object
      properties:
        id:
          type: number
          description: Usa este id en GET /v1/invoices/{id}.
        invoiceNumber:
          type: number
          description: Número de factura correlativo del rango CAI.
        cai:
          type: string
          nullable: true
          description: CAI emitido por el SAR bajo el cual se emitió la factura.
        invoiceDate:
          format: date-time
          type: string
        clientId:
          type: number
          nullable: true
        importe:
          type: number
          description: Suma del importe de las líneas, antes de descuentos.
        discountTotal:
          type: number
        exemptedAmount:
          type: number
          description: Monto no sujeto a ISV (bienes exentos).
        exoneratedAmount:
          type: number
          description: Monto cubierto por una exoneración. Requiere el RTN del cliente.
        totalTaxes:
          type: number
        total:
          type: number
          description: Total general que paga el cliente.
        totalInLetters:
          type: string
          description: Total en letras, tal como lo exige la factura.
        isPaid:
          type: boolean
          description: Siempre true en v1 — solo ventas al contado.
        isAnnulled:
          type: boolean
        taxBreakdown:
          type: array
          items:
            $ref: '#/components/schemas/ExternalInvoiceTaxBreakdownResponseDto'
        lines:
          type: array
          items:
            $ref: '#/components/schemas/ExternalInvoiceLineResponseDto'
        replayed:
          type: boolean
          description: >-
            True cuando esta respuesta es una repetición de una solicitud
            anterior con el mismo Idempotency-Key, en lugar de una factura
            recién creada. Una repetición significa que NO se emitió una segunda
            factura — trátala como un éxito, no como un duplicado.
        pdfBase64:
          type: string
          nullable: true
          description: >-
            El PDF A4 de la factura, codificado en base64. Se incluye solo en
            POST /v1/invoices; en GET /v1/invoices/{id} siempre es null, usa GET
            /v1/invoices/{id}/pdf. Es null si la factura se emitió pero el PDF
            no pudo generarse — la factura es válida de todos modos y el PDF
            sigue disponible en el endpoint dedicado.
      required:
        - id
        - invoiceNumber
        - invoiceDate
        - importe
        - discountTotal
        - exemptedAmount
        - exoneratedAmount
        - totalTaxes
        - total
        - totalInLetters
        - isPaid
        - isAnnulled
        - taxBreakdown
        - lines
        - replayed
    ExternalInvoicePreviewResponseDto:
      type: object
      properties:
        preview:
          type: boolean
          description: >-
            Siempre true. Este documento NO es una factura: no consumió CAI, no
            avanzó el correlativo y no existe en la base de datos.
          example: true
        invoiceDate:
          format: date-time
          type: string
        clientId:
          type: number
          nullable: true
        importe:
          type: number
          description: Suma de líneas antes de descuentos e impuestos.
        discountTotal:
          type: number
        exemptedAmount:
          type: number
          description: EXENTO — producto no gravado.
        exoneratedAmount:
          type: number
          description: EXONERADO — respaldado por documento legal.
        totalTaxes:
          type: number
        total:
          type: number
        totalInLetters:
          type: string
        appliedDollarExchangeRate:
          type: number
          nullable: true
          description: Lempiras por USD aplicados. Null si ninguna línea estaba en dólares.
        taxBreakdown:
          type: array
          items:
            $ref: '#/components/schemas/ExternalInvoiceTaxBreakdownResponseDto'
        lines:
          type: array
          items:
            $ref: '#/components/schemas/ExternalInvoiceLineResponseDto'
        pdfBase64:
          type: string
          description: >-
            PDF A4 en base64, idéntico al que imprime Contadito pero marcado SIN
            VALIDEZ FISCAL. No entregable a un cliente final.
      required:
        - preview
        - invoiceDate
        - importe
        - discountTotal
        - exemptedAmount
        - exoneratedAmount
        - totalTaxes
        - total
        - totalInLetters
        - taxBreakdown
        - lines
        - pdfBase64
