# Generado por OpenApiSpecService — NO editar a mano.
# Fuente: app/Services/ApiExt/OpenApiSpecService.php
# Regenerar: php spark apiext:openapi-build
openapi: 3.0.3
info:
  title: Facter — API de Timbrado para Integradores
  version: 2.0.0
  description: |-
    API pública para timbrar **CFDI 4.0** usando la infraestructura de Facter.
    
    **Modelo comercial:** un **cliente principal** (plan PREPAGO) compra timbres; sus
    **emisores** (cada uno un tenant con su propio RFC y CSD) consumen del saldo del principal.
    
    El contrato de entrada es el **JSON canónico CFDI 4.0**: el integrador envía el comprobante
    completo (Emisor, Receptor, Conceptos, Impuestos, Complemento...). Facter **valida** consistencia
    aritmética y contra catálogos SAT pero **no recalcula** totales — tu sistema es el sistema de registro.
    
    **Ambientes:** prueba en **Demo** `https://demo.facter.com.mx/api/ext/v1` (sandbox de pruebas, sin
    cobro real, datos efímeros) y, cuando estés listo, pasa a **Producción** `https://v2.facter.com.mx/api/ext/v1`
    (timbrado real, consume timbres). Necesitas **una cuenta y una API key por ambiente**; la key de un
    ambiente no funciona en el otro. El host define el comportamiento.
    
    **Autenticación:** `Authorization: Bearer fct_live_xxx`.
    **Idempotencia obligatoria** en endpoints mutadores vía header `Idempotency-Key`.
    Toda respuesta incluye `X-Request-Id` para soporte.
    
    Documentación completa, quickstart y ejemplos: https://v2.facter.com.mx/developers
  contact:
    name: Soporte a integradores Facter
    url: 'https://demo.facter.com.mx/developers'
    email: 'soporte@facter.com.mx'
  'x-versioning': |-
    Versión en la ruta (`/v1/`). Cambios incompatibles → `/v2/`.
    Agregar campos a las respuestas NO se considera breaking: el cliente debe tolerar campos desconocidos.
    Las deprecaciones se anuncian con header `Sunset` en las respuestas afectadas.
servers:
  - url: 'https://demo.facter.com.mx/api/ext/v1'
    description: 'Demo (pruebas, sandbox de pruebas — sin cobro real)'
  - url: 'https://v2.facter.com.mx/api/ext/v1'
    description: 'Producción (timbrado real, consume timbres)'
security:
  - bearerAuth: []
tags:
  - name: CFDIs
    description: 'Timbrado, validación, consulta y descarga de comprobantes.'
  - name: Cancelaciones
    description: Solicitud y seguimiento de cancelaciones SAT (asíncrono).
  - name: Emisores
    description: Alta de emisores (tenants) y carga de CSD por API.
  - name: Timbres
    description: Saldo del pool del cliente principal y reporte de consumo.
  - name: Webhooks
    description: 'Recepción de notificaciones asíncronas. **Los webhooks se configuran desde la aplicación web** (Mi cuenta → Webhooks), **no por API**: esta sección documenta cómo tu endpoint **recibe y verifica** los eventos. Cada entrega lleva el header `X-Facter-Signature: sha256=<HMAC del cuerpo con tu webhook_secret>` (verifica con comparación en tiempo constante) y reintenta con backoff 1m, 5m, 30m, 2h, 12h → DLQ. Los eventos (`cfdi.timbrado`, `cfdi.cancelado`, `cfdi.cancelacion_rechazada`, `timbres.bajo_saldo`, `emisor.csd_por_vencer`) se disparan para los CFDIs de tu cuenta **sin importar el canal**: tanto los timbrados/cancelados por este API como los emitidos desde la **operación web** de Facter. El payload de `cfdi.timbrado` incluye un campo `origin` (`API` | `WEB`) para distinguir el canal; en CFDIs timbrados por web `external_ref` es `null`.'
paths:
  '/cfdis':
    post:
      tags:
        - CFDIs
      summary: Timbrar CFDI
      operationId: stampCfdi
      description: |-
        Endpoint principal. Timbra un CFDI 4.0 de tipo **I** (ingreso), **P** (Pagos 2.0) o **E** (Nota de Crédito/Egreso).
        Consume un timbre del pool del principal **solo si el timbrado es exitoso**.
        
        `emisor_rfc` también puede ser **el RFC de tu propia cuenta principal**: queda registrada automáticamente como emisor (`is_principal: true` en `GET /emisores`) y timbra consumiendo su propio saldo PREPAGO.
        
        **Ambiente demo**: contra `demo.facter.com.mx` el timbrado usa el sandbox de pruebas (sin cobro real) y la respuesta agrega `"environment": "demo"`. El pipeline es idéntico al de producción (persiste y consume del pool).
        **Conciliación**: si el PAC timbró pero la persistencia/consumo falló, la respuesta agrega `"reconciliation_pending": true`; trátala como exitosa.
      security:
        - bearerAuth: []
      'x-scope': 'cfdi:stamp'
      parameters:
        - '$ref': '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              '$ref': '#/components/schemas/StampRequest'
            example:
              emisor_rfc: EKU9003173C9
              external_ref: REF-FAC-000123
              cfdi:
                Version: '4.0'
                Serie: A
                Folio: '123'
                FormaPago: '01'
                MetodoPago: PUE
                SubTotal: '1000.00'
                Moneda: MXN
                Total: '1160.00'
                TipoDeComprobante: I
                Exportacion: '01'
                LugarExpedicion: '64000'
                Emisor:
                  Rfc: EKU9003173C9
                  Nombre: ESCUELA KEMPER URGATE
                  RegimenFiscal: '601'
                Receptor:
                  Rfc: XAXX010101000
                  Nombre: PUBLICO EN GENERAL
                  DomicilioFiscalReceptor: '64000'
                  RegimenFiscalReceptor: '616'
                  UsoCFDI: S01
                Conceptos:
                  - ClaveProdServ: '01010101'
                    NoIdentificacion: SKU-01
                    Cantidad: '1.000000'
                    ClaveUnidad: H87
                    Unidad: Pieza
                    Descripcion: Producto de prueba
                    ValorUnitario: '1000.00'
                    Importe: '1000.00'
                    ObjetoImp: '02'
                    Impuestos:
                      Traslados:
                        - Base: '1000.00'
                          Impuesto: '002'
                          TipoFactor: Tasa
                          TasaOCuota: '0.160000'
                          Importe: '160.00'
                      Retenciones: []
                Impuestos:
                  TotalImpuestosTrasladados: '160.00'
                  Traslados:
                    - Base: '1000.00'
                      Impuesto: '002'
                      TipoFactor: Tasa
                      TasaOCuota: '0.160000'
                      Importe: '160.00'
      responses:
        '201':
          description: CFDI timbrado correctamente
          headers:
            'X-Request-Id':
              description: UUID de la petición (api_requests) para soporte y trazabilidad.
              schema:
                type: string
                format: uuid
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/StampResponse'
              example:
                status: success
                message: CFDI timbrado correctamente
                data:
                  uuid: AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE
                  external_ref: REF-FAC-000123
                  emisor_rfc: EKU9003173C9
                  serie: A
                  folio: '123'
                  total: '1160.00'
                  fecha_emision: '2026-06-10T12:34:56'
                  fecha_timbrado: '2026-06-10T12:34:58'
                  sello_sat: GqDjX...
                  no_certificado_sat: '00001000000504465028'
                  xml: '<?xml version="1.0" encoding="UTF-8"?><cfdi:Comprobante ...>'
                  links:
                    self: /api/ext/v1/cfdis/AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE
                    xml: /api/ext/v1/cfdis/AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE/xml
                    pdf: /api/ext/v1/cfdis/AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE/pdf
                  timbres:
                    consumidos: 1
                    saldo_restante: 4987
        '400':
          '$ref': '#/components/responses/InvalidPayload'
        '401':
          '$ref': '#/components/responses/InvalidApiKey'
        '402':
          '$ref': '#/components/responses/NoStampsAvailable'
        '403':
          '$ref': '#/components/responses/EmisorNotOwned'
        '409':
          '$ref': '#/components/responses/IdempotencyConflict'
        '413':
          '$ref': '#/components/responses/PayloadTooLarge'
        '422':
          '$ref': '#/components/responses/FiscalValidation'
        '429':
          '$ref': '#/components/responses/RateLimited'
        '500':
          '$ref': '#/components/responses/InternalError'
    get:
      tags:
        - CFDIs
      summary: Listar CFDIs
      operationId: listCfdis
      description: 'Listado paginado de CFDIs de los emisores del cliente. Orden por defecto: `fecha_emision DESC`.'
      'x-scope': 'cfdi:read'
      parameters:
        - name: emisor_rfc
          in: query
          required: false
          description: Filtra por RFC del emisor.
          schema:
            type: string
          example: EKU9003173C9
        - name: fecha_desde
          in: query
          required: false
          description: Fecha mínima (YYYY-MM-DD) sobre fecha_emision.
          schema:
            type: string
          example: '2026-06-01'
        - name: fecha_hasta
          in: query
          required: false
          description: Fecha máxima (YYYY-MM-DD) sobre fecha_emision.
          schema:
            type: string
          example: '2026-06-30'
        - name: serie
          in: query
          required: false
          description: Filtra por serie.
          schema:
            type: string
          example: A
        - name: folio
          in: query
          required: false
          description: Filtra por folio.
          schema:
            type: string
          example: '123'
        - name: external_ref
          in: query
          required: false
          description: Filtra por referencia del integrador.
          schema:
            type: string
          example: REF-FAC-000123
        - name: origin
          in: query
          required: false
          description: Origen del CFDI.
          schema:
            type: string
            enum:
              - API
              - UI
              - EXTERNAL
          example: API
        - name: status
          in: query
          required: false
          description: Estatus del CFDI.
          schema:
            type: string
            enum:
              - vigente
              - cancelado
          example: vigente
        - name: page
          in: query
          required: false
          description: Página (default 1).
          schema:
            type: integer
          example: 1
        - name: per_page
          in: query
          required: false
          description: 'Resultados por página (default 25, máx 100).'
          schema:
            type: integer
          example: 25
      responses:
        '200':
          description: Listado paginado
          headers:
            'X-Request-Id':
              description: UUID de la petición (api_requests) para soporte y trazabilidad.
              schema:
                type: string
                format: uuid
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/CfdiListResponse'
        '401':
          '$ref': '#/components/responses/InvalidApiKey'
        '403':
          '$ref': '#/components/responses/InsufficientScope'
        '429':
          '$ref': '#/components/responses/RateLimited'
  '/cfdis/validate':
    post:
      tags:
        - CFDIs
      summary: Validar sin timbrar (dry-run)
      operationId: validateCfdi
      description: 'Ejecuta TODO el pipeline de validación (estructura, aritmética, catálogos SAT, FiscalValidation, CSD vigente, saldo) **sin sellar ni timbrar**. No consume timbre ni requiere Idempotency-Key.'
      'x-scope': 'cfdi:stamp'
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              '$ref': '#/components/schemas/StampRequest'
            example:
              emisor_rfc: EKU9003173C9
              external_ref: REF-FAC-000123
              cfdi:
                Version: '4.0'
                Serie: A
                Folio: '123'
                FormaPago: '01'
                MetodoPago: PUE
                SubTotal: '1000.00'
                Moneda: MXN
                Total: '1160.00'
                TipoDeComprobante: I
                Exportacion: '01'
                LugarExpedicion: '64000'
                Emisor:
                  Rfc: EKU9003173C9
                  Nombre: ESCUELA KEMPER URGATE
                  RegimenFiscal: '601'
                Receptor:
                  Rfc: XAXX010101000
                  Nombre: PUBLICO EN GENERAL
                  DomicilioFiscalReceptor: '64000'
                  RegimenFiscalReceptor: '616'
                  UsoCFDI: S01
                Conceptos:
                  - ClaveProdServ: '01010101'
                    NoIdentificacion: SKU-01
                    Cantidad: '1.000000'
                    ClaveUnidad: H87
                    Unidad: Pieza
                    Descripcion: Producto de prueba
                    ValorUnitario: '1000.00'
                    Importe: '1000.00'
                    ObjetoImp: '02'
                    Impuestos:
                      Traslados:
                        - Base: '1000.00'
                          Impuesto: '002'
                          TipoFactor: Tasa
                          TasaOCuota: '0.160000'
                          Importe: '160.00'
                      Retenciones: []
                Impuestos:
                  TotalImpuestosTrasladados: '160.00'
                  Traslados:
                    - Base: '1000.00'
                      Impuesto: '002'
                      TipoFactor: Tasa
                      TasaOCuota: '0.160000'
                      Importe: '160.00'
      responses:
        '200':
          description: Validación completada (revisar data.valid)
          headers:
            'X-Request-Id':
              description: UUID de la petición (api_requests) para soporte y trazabilidad.
              schema:
                type: string
                format: uuid
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/ValidateResponse'
              example:
                status: success
                message: Validación completada
                data:
                  valid: true
                  errors: []
                  warnings:
                    - code: RESICO-ISR-RET
                      message: 'El receptor sugiere retención ISR 1.25% (RESICO PF)'
        '401':
          '$ref': '#/components/responses/InvalidApiKey'
        '403':
          '$ref': '#/components/responses/InsufficientScope'
        '429':
          '$ref': '#/components/responses/RateLimited'
  '/cfdis/{uuid}':
    get:
      tags:
        - CFDIs
      summary: Consultar CFDI
      operationId: getCfdi
      description: 'Detalle de un CFDI. `404 CFDI_NOT_FOUND` si el UUID no pertenece a un emisor del cliente (no se revela su existencia).'
      'x-scope': 'cfdi:read'
      parameters:
        - '$ref': '#/components/parameters/UuidPath'
      responses:
        '200':
          description: Detalle del CFDI
          headers:
            'X-Request-Id':
              description: UUID de la petición (api_requests) para soporte y trazabilidad.
              schema:
                type: string
                format: uuid
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/CfdiDetailResponse'
              example:
                status: success
                data:
                  uuid: AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE
                  external_ref: REF-FAC-000123
                  emisor_rfc: EKU9003173C9
                  receptor_rfc: XAXX010101000
                  serie: A
                  folio: '123'
                  total: '1160.00'
                  moneda: MXN
                  tipo_de_comprobante: I
                  fecha_emision: '2026-06-10T12:34:56'
                  fecha_timbrado: '2026-06-10T12:34:58'
                  status: vigente
                  origin: API
                  links:
                    self: /api/ext/v1/cfdis/AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE
                    xml: /api/ext/v1/cfdis/AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE/xml
                    pdf: /api/ext/v1/cfdis/AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE/pdf
        '401':
          '$ref': '#/components/responses/InvalidApiKey'
        '404':
          '$ref': '#/components/responses/CfdiNotFound'
  '/cfdis/{uuid}/xml':
    get:
      tags:
        - CFDIs
      summary: Descargar XML timbrado
      operationId: getCfdiXml
      description: 'Descarga el XML timbrado del CFDI desde el almacenamiento (Content-Disposition: attachment).'
      'x-scope': 'cfdi:read'
      parameters:
        - '$ref': '#/components/parameters/UuidPath'
      responses:
        '200':
          description: 'XML del CFDI (Content-Disposition: attachment)'
          content:
            'application/xml':
              schema:
                type: string
                format: binary
        '401':
          '$ref': '#/components/responses/InvalidApiKey'
        '404':
          '$ref': '#/components/responses/CfdiNotFound'
  '/cfdis/{uuid}/pdf':
    get:
      tags:
        - CFDIs
      summary: PDF generado al vuelo
      operationId: getCfdiPdf
      description: PDF generado dinámicamente desde el XML con la plantilla fiscal del emisor (no se persiste).
      'x-scope': 'cfdi:read'
      parameters:
        - '$ref': '#/components/parameters/UuidPath'
      responses:
        '200':
          description: 'PDF del CFDI (Content-Disposition: inline)'
          content:
            'application/pdf':
              schema:
                type: string
                format: binary
        '401':
          '$ref': '#/components/responses/InvalidApiKey'
        '404':
          '$ref': '#/components/responses/CfdiNotFound'
  '/cfdis/{uuid}/cancelacion':
    post:
      tags:
        - Cancelaciones
      summary: Solicitar cancelación SAT
      operationId: requestCancellation
      description: 'Solicita la cancelación ante el SAT. Respuesta `202 Accepted`: el proceso es asíncrono (puede tardar hasta 72 h hábiles con aceptación del receptor). Seguir por GET o webhook `cfdi.cancelado`.'
      'x-scope': 'cfdi:cancel'
      parameters:
        - '$ref': '#/components/parameters/UuidPath'
        - '$ref': '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              '$ref': '#/components/schemas/CancellationRequest'
            example:
              motivo: '02'
      responses:
        '202':
          description: Cancelación solicitada (asíncrona)
          headers:
            'X-Request-Id':
              description: UUID de la petición (api_requests) para soporte y trazabilidad.
              schema:
                type: string
                format: uuid
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/CancellationStatusResponse'
              example:
                status: success
                message: Cancelación solicitada
                data:
                  uuid: AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE
                  cancel_status: SOLICITADO_201
        '401':
          '$ref': '#/components/responses/InvalidApiKey'
        '403':
          '$ref': '#/components/responses/InsufficientScope'
        '404':
          '$ref': '#/components/responses/CfdiNotFound'
        '409':
          '$ref': '#/components/responses/IdempotencyConflict'
        '422':
          '$ref': '#/components/responses/InvalidPayload'
    get:
      tags:
        - Cancelaciones
      summary: Estatus de cancelación
      operationId: getCancellationStatus
      description: Devuelve el estado actual del flujo de cancelación SAT del CFDI.
      'x-scope': 'cfdi:read'
      parameters:
        - '$ref': '#/components/parameters/UuidPath'
      responses:
        '200':
          description: Estatus actual de la cancelación
          headers:
            'X-Request-Id':
              description: UUID de la petición (api_requests) para soporte y trazabilidad.
              schema:
                type: string
                format: uuid
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/CancellationStatusResponse'
              example:
                status: success
                data:
                  uuid: AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE
                  cancel_status: EN_PROCESO_ACEPTACION
        '401':
          '$ref': '#/components/responses/InvalidApiKey'
        '404':
          '$ref': '#/components/responses/CfdiNotFound'
  '/emisores':
    post:
      tags:
        - Emisores
      summary: Dar de alta un emisor
      operationId: createEmisor
      description: 'Da de alta un emisor a partir de su RFC. Si el RFC NO existe como cuenta, crea el tenant hijo (branch default, timbrado activo, billing PREPAGO saldo 0). Si el RFC YA existe como una cuenta libre (sin pertenecer a ningún pool ni ser principal/source), reusa esa cuenta sin tocar sus datos fiscales (responde 200 con `reused: true`). En ambos casos la vincula al cliente y la agrega al pool de timbres del principal. Transaccional con rollback total.'
      'x-scope': 'emisores:manage'
      parameters:
        - '$ref': '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              '$ref': '#/components/schemas/EmisorCreateRequest'
            example:
              external_ref: tenant-externo-42
              razon_social: ESCUELA KEMPER URGATE
              rfc: EKU9003173C9
              regimen_fiscal: '601'
              codigo_postal: '64000'
      responses:
        '200':
          description: 'Emisor existente (mismo RFC) vinculado al cliente. El cuerpo incluye `reused: true`.'
          headers:
            'X-Request-Id':
              description: UUID de la petición (api_requests) para soporte y trazabilidad.
              schema:
                type: string
                format: uuid
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/EmisorResponse'
        '201':
          description: Emisor creado
          headers:
            'X-Request-Id':
              description: UUID de la petición (api_requests) para soporte y trazabilidad.
              schema:
                type: string
                format: uuid
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/EmisorResponse'
        '401':
          '$ref': '#/components/responses/InvalidApiKey'
        '403':
          '$ref': '#/components/responses/InsufficientScope'
        '409':
          '$ref': '#/components/responses/EmisorConflict'
        '422':
          '$ref': '#/components/responses/InvalidPayload'
    get:
      tags:
        - Emisores
      summary: Listar emisores
      operationId: listEmisores
      description: 'Lista los emisores (tenants) vinculados al cliente del API key. Incluye a tu propia cuenta principal (registrada automáticamente como emisor de su RFC), identificable por `is_principal: true`.'
      'x-scope': 'emisores:manage'
      responses:
        '200':
          description: Listado de emisores del cliente
          headers:
            'X-Request-Id':
              description: UUID de la petición (api_requests) para soporte y trazabilidad.
              schema:
                type: string
                format: uuid
          content:
            'application/json':
              schema:
                type: object
                properties:
                  status:
                    type: string
                  data:
                    type: array
                    items:
                      '$ref': '#/components/schemas/Emisor'
        '401':
          '$ref': '#/components/responses/InvalidApiKey'
        '403':
          '$ref': '#/components/responses/InsufficientScope'
  '/emisores/{rfc}':
    get:
      tags:
        - Emisores
      summary: Detalle de emisor (incluye estatus CSD)
      operationId: getEmisor
      description: 'Detalle del emisor, incluyendo el estatus y vigencia de su CSD.'
      'x-scope': 'emisores:manage'
      parameters:
        - '$ref': '#/components/parameters/RfcPath'
      responses:
        '200':
          description: Detalle del emisor
          headers:
            'X-Request-Id':
              description: UUID de la petición (api_requests) para soporte y trazabilidad.
              schema:
                type: string
                format: uuid
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/EmisorResponse'
        '401':
          '$ref': '#/components/responses/InvalidApiKey'
        '404':
          '$ref': '#/components/responses/EmisorNotFound'
  '/emisores/{rfc}/csd':
    put:
      tags:
        - Emisores
      summary: Subir/reemplazar CSD
      operationId: uploadCsd
      description: 'Carga el CSD del emisor. Validaciones sin excepción: que sea CSD y no FIEL (Key Usage), vigencia, correspondencia cert/key, RFC del certificado = RFC del emisor. Reemplazo atómico (valida antes de tocar el CSD vigente).'
      'x-scope': 'emisores:manage'
      parameters:
        - '$ref': '#/components/parameters/RfcPath'
      requestBody:
        required: true
        content:
          'application/json':
            schema:
              '$ref': '#/components/schemas/CsdUploadRequest'
            example:
              cer_base64: MIIF...
              key_base64: MIIE...
              password: ••••••••
      responses:
        '200':
          description: CSD cargado y validado
          headers:
            'X-Request-Id':
              description: UUID de la petición (api_requests) para soporte y trazabilidad.
              schema:
                type: string
                format: uuid
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/CsdUploadResponse'
              example:
                status: success
                message: CSD cargado
                data:
                  valid_from: '2025-01-01T00:00:00'
                  valid_to: '2029-01-01T00:00:00'
                  serial: '00001000000504465028'
        '401':
          '$ref': '#/components/responses/InvalidApiKey'
        '404':
          '$ref': '#/components/responses/EmisorNotFound'
        '422':
          '$ref': '#/components/responses/CsdInvalid'
  '/timbres':
    get:
      tags:
        - Timbres
      summary: Saldo del pool del principal
      operationId: getTimbres
      description: Saldo del pool de timbres del cliente principal y consumo del mes en curso por emisor.
      'x-scope': 'timbres:read'
      responses:
        '200':
          description: Saldo y consumo del mes
          headers:
            'X-Request-Id':
              description: UUID de la petición (api_requests) para soporte y trazabilidad.
              schema:
                type: string
                format: uuid
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/TimbresResponse'
              example:
                status: success
                data:
                  saldo: 4987
                  consumidos_mes_actual: 1013
                  por_emisor_mes_actual:
                    - emisor_rfc: EKU9003173C9
                      external_ref: tenant-externo-42
                      consumidos: 512
        '401':
          '$ref': '#/components/responses/InvalidApiKey'
        '403':
          '$ref': '#/components/responses/InsufficientScope'
  '/timbres/reporte':
    get:
      tags:
        - Timbres
      summary: Reporte de consumo
      operationId: getTimbresReporte
      description: 'Desglose de consumo por emisor y por día en el periodo. Periodo máximo: 12 meses.'
      'x-scope': 'timbres:read'
      parameters:
        - name: desde
          in: query
          required: false
          description: Fecha inicial (YYYY-MM-DD).
          schema:
            type: string
          example: '2026-06-01'
        - name: hasta
          in: query
          required: false
          description: Fecha final (YYYY-MM-DD).
          schema:
            type: string
          example: '2026-06-30'
      responses:
        '200':
          description: Reporte de consumo
          headers:
            'X-Request-Id':
              description: UUID de la petición (api_requests) para soporte y trazabilidad.
              schema:
                type: string
                format: uuid
          content:
            'application/json':
              schema:
                '$ref': '#/components/schemas/TimbresResponse'
        '401':
          '$ref': '#/components/responses/InvalidApiKey'
        '403':
          '$ref': '#/components/responses/InsufficientScope'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |-
        API key del cliente con prefijo `fct_live_`. El comportamiento lo define el ambiente
        (host), no el prefijo: una key emitida en Demo solo funciona contra `demo.facter.com.mx` y
        una de Producción contra `v2.facter.com.mx`. Se envía como `Authorization: Bearer fct_live_xxx`.
        La key se guarda hasheada (SHA-256) en reposo; trátala como secreto y NUNCA la expongas en frontend.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: UUID único por intento de operación. Reintentar con la MISMA key devuelve el mismo resultado sin volver a ejecutar (ni cobrar) la operación.
      schema:
        type: string
        format: uuid
      example: a3f1c2e4-9b8d-4c7a-8e2f-1d6b5a4c3e2f
    UuidPath:
      name: uuid
      in: path
      required: true
      description: UUID (Folio Fiscal) del CFDI.
      schema:
        type: string
        format: uuid
      example: AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE
    RfcPath:
      name: rfc
      in: path
      required: true
      description: RFC del emisor (tenant) del cliente.
      schema:
        type: string
      example: EKU9003173C9
  responses:
    InvalidPayload:
      description: JSON malformado o campos requeridos faltantes
      content:
        'application/json':
          schema:
            '$ref': '#/components/schemas/ErrorEnvelope'
          example:
            status: error
            message: JSON malformado o campos requeridos faltantes
            code: INVALID_PAYLOAD
            errors:
              - code: CFDI40-TOTAL
                field: cfdi.Total
                message: 'El Total no coincide: SubTotal - Descuento + Traslados - Retenciones = 1160.00, se recibió 1150.00'
    InvalidApiKey:
      description: 'Key inexistente, revocada o expirada'
      content:
        'application/json':
          schema:
            '$ref': '#/components/schemas/ErrorEnvelope'
          example:
            status: error
            message: 'Key inexistente, revocada o expirada'
            code: INVALID_API_KEY
    NoStampsAvailable:
      description: El pool del principal no tiene timbres disponibles
      content:
        'application/json':
          schema:
            '$ref': '#/components/schemas/ErrorEnvelope'
          example:
            status: error
            message: El pool del principal no tiene timbres disponibles
            code: NO_STAMPS_AVAILABLE
    InsufficientScope:
      description: La API key no tiene el scope requerido (code también puede ser CLIENT_SUSPENDED o IP_NOT_ALLOWED para el mismo HTTP 403)
      content:
        'application/json':
          schema:
            '$ref': '#/components/schemas/ErrorEnvelope'
          example:
            status: error
            message: La API key no tiene el scope requerido (code también puede ser CLIENT_SUSPENDED o IP_NOT_ALLOWED para el mismo HTTP 403)
            code: INSUFFICIENT_SCOPE
    EmisorNotOwned:
      description: 'El emisor no pertenece a este cliente, está suspendido (EMISOR_SUSPENDED) o el acceso fue denegado (CLIENT_SUSPENDED / IP_NOT_ALLOWED / INSUFFICIENT_SCOPE). El RFC de tu cuenta principal queda ligado automáticamente al activar tu API; si aun así lo ves con tu propio RFC, genera (o regenera) una API key para forzar el alta del self-emisor'
      content:
        'application/json':
          schema:
            '$ref': '#/components/schemas/ErrorEnvelope'
          example:
            status: error
            message: 'El emisor no pertenece a este cliente, está suspendido (EMISOR_SUSPENDED) o el acceso fue denegado (CLIENT_SUSPENDED / IP_NOT_ALLOWED / INSUFFICIENT_SCOPE). El RFC de tu cuenta principal queda ligado automáticamente al activar tu API; si aun así lo ves con tu propio RFC, genera (o regenera) una API key para forzar el alta del self-emisor'
            code: EMISOR_NOT_OWNED
    IdempotencyConflict:
      description: 'Misma Idempotency-Key con un cuerpo distinto. Si la operación sigue en curso, el code es IDEMPOTENCY_IN_FLIGHT: reintenta en unos segundos'
      content:
        'application/json':
          schema:
            '$ref': '#/components/schemas/ErrorEnvelope'
          example:
            status: error
            message: 'Misma Idempotency-Key con un cuerpo distinto. Si la operación sigue en curso, el code es IDEMPOTENCY_IN_FLIGHT: reintenta en unos segundos'
            code: IDEMPOTENCY_CONFLICT
    EmisorConflict:
      description: 'Conflicto al vincular el emisor. El code puede ser: TENANT_ALREADY_IN_POOL (el RFC ya pertenece a un pool de timbres), TENANT_IS_API_PRINCIPAL (el RFC ya es principal de un cliente API), TENANT_IS_POOL_SOURCE (el RFC presta timbres a un pool), TENANT_IS_PRINCIPAL (es la propia cuenta principal), TENANT_RFC_AMBIGUOUS (hay más de una cuenta activa con ese RFC), EMISOR_RFC_DUPLICATE, EMISOR_LIMIT_REACHED, o los de idempotencia IDEMPOTENCY_CONFLICT / IDEMPOTENCY_IN_FLIGHT'
      content:
        'application/json':
          schema:
            '$ref': '#/components/schemas/ErrorEnvelope'
          example:
            status: error
            message: 'Conflicto al vincular el emisor. El code puede ser: TENANT_ALREADY_IN_POOL (el RFC ya pertenece a un pool de timbres), TENANT_IS_API_PRINCIPAL (el RFC ya es principal de un cliente API), TENANT_IS_POOL_SOURCE (el RFC presta timbres a un pool), TENANT_IS_PRINCIPAL (es la propia cuenta principal), TENANT_RFC_AMBIGUOUS (hay más de una cuenta activa con ese RFC), EMISOR_RFC_DUPLICATE, EMISOR_LIMIT_REACHED, o los de idempotencia IDEMPOTENCY_CONFLICT / IDEMPOTENCY_IN_FLIGHT'
            code: TENANT_ALREADY_IN_POOL
    FiscalValidation:
      description: 'El CFDI no pasó las validaciones fiscales (también CSD_INVALID si el CSD del emisor es inválido/vencido). Si envías un TipoDeComprobante que el API aún no soporta —Nómina (N) o Traslado (T)— el code es UNSUPPORTED_COMPROBANTE_TYPE: hoy solo se timbran I (Ingreso), E (Egreso/Nota de Crédito) y P (Pago). Si el SAT/PAC rechaza el comprobante, el code del envelope es PAC_REJECTED y tanto message como errors[].code/message traen el detalle accionable del SAT —p.ej. el código de regla CFDI40130 y su descripción—, no un mensaje genérico'
      content:
        'application/json':
          schema:
            '$ref': '#/components/schemas/ErrorEnvelope'
          example:
            status: error
            message: 'El CFDI no pasó las validaciones fiscales (también CSD_INVALID si el CSD del emisor es inválido/vencido). Si envías un TipoDeComprobante que el API aún no soporta —Nómina (N) o Traslado (T)— el code es UNSUPPORTED_COMPROBANTE_TYPE: hoy solo se timbran I (Ingreso), E (Egreso/Nota de Crédito) y P (Pago). Si el SAT/PAC rechaza el comprobante, el code del envelope es PAC_REJECTED y tanto message como errors[].code/message traen el detalle accionable del SAT —p.ej. el código de regla CFDI40130 y su descripción—, no un mensaje genérico'
            code: FISCAL_VALIDATION_FAILED
            errors:
              - code: RESICO-101
                field: cfdi.Receptor
                message: 'El receptor RESICO PF requiere retención ISR 1.25%'
    CsdInvalid:
      description: 'El emisor no tiene CSD, está vencido o es inválido'
      content:
        'application/json':
          schema:
            '$ref': '#/components/schemas/ErrorEnvelope'
          example:
            status: error
            message: 'El emisor no tiene CSD, está vencido o es inválido'
            code: CSD_INVALID
    PayloadTooLarge:
      description: El cuerpo excede el tamaño máximo permitido (1 MB)
      content:
        'application/json':
          schema:
            '$ref': '#/components/schemas/ErrorEnvelope'
          example:
            status: error
            message: El cuerpo excede el tamaño máximo permitido (1 MB)
            code: PAYLOAD_TOO_LARGE
    RateLimited:
      description: Límite de tasa excedido. Incluye header Retry-After.
      headers:
        'Retry-After':
          schema:
            type: integer
          description: Segundos a esperar antes de reintentar.
      content:
        'application/json':
          schema:
            '$ref': '#/components/schemas/ErrorEnvelope'
          example:
            status: error
            message: 'Demasiadas peticiones, reintenta más tarde'
            code: RATE_LIMITED
    CfdiNotFound:
      description: El UUID no pertenece a un emisor de este cliente
      content:
        'application/json':
          schema:
            '$ref': '#/components/schemas/ErrorEnvelope'
          example:
            status: error
            message: El UUID no pertenece a un emisor de este cliente
            code: CFDI_NOT_FOUND
    EmisorNotFound:
      description: El RFC no es un emisor de este cliente
      content:
        'application/json':
          schema:
            '$ref': '#/components/schemas/ErrorEnvelope'
          example:
            status: error
            message: El RFC no es un emisor de este cliente
            code: EMISOR_NOT_FOUND
    InternalError:
      description: 'Error interno. Usa X-Request-Id para soporte. Si el timbre se consumió, la conciliación garantiza entregar el CFDI'
      content:
        'application/json':
          schema:
            '$ref': '#/components/schemas/ErrorEnvelope'
          example:
            status: error
            message: 'Error interno. Usa X-Request-Id para soporte. Si el timbre se consumió, la conciliación garantiza entregar el CFDI'
            code: INTERNAL_ERROR
  schemas:
    ErrorEnvelope:
      type: object
      required:
        - status
        - message
        - code
      properties:
        status:
          type: string
          enum:
            - error
          example: error
        message:
          type: string
          description: Mensaje legible en español.
          example: El CFDI no pasó las validaciones fiscales
        code:
          type: string
          description: Código de negocio estable (machine-readable).
          example: FISCAL_VALIDATION_FAILED
        docs:
          type: string
          format: uri
          description: Link a la sección de manejo de errores del portal.
          example: 'https://demo.facter.com.mx/developers#errores'
        errors:
          type: array
          description: Detalle por campo/regla (cuando aplica).
          items:
            type: object
            properties:
              code:
                type: string
                example: CFDI40-TOTAL
              field:
                type: string
                nullable: true
                example: cfdi.Total
              message:
                type: string
                example: El Total no coincide con la suma de conceptos e impuestos
    StampRequest:
      type: object
      required:
        - emisor_rfc
        - cfdi
      properties:
        emisor_rfc:
          type: string
          description: RFC de un emisor ACTIVO del cliente con CSD vigente. Debe coincidir con cfdi.Emisor.Rfc. Puede ser el RFC de tu propia cuenta principal (timbra contra su propio saldo PREPAGO).
          example: EKU9003173C9
        external_ref:
          type: string
          nullable: true
          description: Referencia del integrador; se devuelve en consultas y webhooks.
          example: REF-FAC-000123
        fecha_emision:
          type: string
          nullable: true
          description: 'Opcional. Si se envía: ventana máx. 72 h hacia atrás, nunca futura (zona horaria por CP). Si es null, la fija Facter al timbrar.'
        cfdi:
          '$ref': '#/components/schemas/Comprobante'
    StampResponse:
      type: object
      properties:
        status:
          type: string
          example: success
        message:
          type: string
          example: CFDI timbrado correctamente
        data:
          type: object
          properties:
            uuid:
              type: string
              format: uuid
            external_ref:
              type: string
              nullable: true
            emisor_rfc:
              type: string
            serie:
              type: string
              nullable: true
            folio:
              type: string
              nullable: true
            total:
              type: string
            fecha_emision:
              type: string
            fecha_timbrado:
              type: string
            sello_sat:
              type: string
            no_certificado_sat:
              type: string
            xml:
              type: string
              description: XML timbrado completo.
            environment:
              type: string
              enum:
                - demo
              description: Presente solo en el ambiente demo (timbrado contra sandbox de pruebas).
            reconciliation_pending:
              type: boolean
              description: Presente solo si la persistencia/consumo está pendiente de conciliación; el CFDI es válido.
            links:
              '$ref': '#/components/schemas/CfdiLinks'
            timbres:
              type: object
              properties:
                consumidos:
                  type: integer
                saldo_restante:
                  type: integer
    ValidateResponse:
      type: object
      properties:
        status:
          type: string
          example: success
        message:
          type: string
        data:
          type: object
          properties:
            valid:
              type: boolean
            errors:
              type: array
              items:
                type: object
            warnings:
              type: array
              items:
                type: object
    CfdiLinks:
      type: object
      properties:
        self:
          type: string
        xml:
          type: string
        pdf:
          type: string
    CfdiSummary:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
        external_ref:
          type: string
          nullable: true
        emisor_rfc:
          type: string
        receptor_rfc:
          type: string
        serie:
          type: string
          nullable: true
        folio:
          type: string
          nullable: true
        total:
          type: string
        moneda:
          type: string
        tipo_de_comprobante:
          type: string
          enum:
            - I
            - E
            - P
            - N
        fecha_emision:
          type: string
        fecha_timbrado:
          type: string
        status:
          type: string
          enum:
            - vigente
            - cancelado
            - en_proceso_cancelacion
        origin:
          type: string
          enum:
            - API
            - UI
            - EXTERNAL
        links:
          '$ref': '#/components/schemas/CfdiLinks'
    CfdiDetailResponse:
      type: object
      properties:
        status:
          type: string
          example: success
        data:
          '$ref': '#/components/schemas/CfdiSummary'
    CfdiListResponse:
      type: object
      properties:
        status:
          type: string
          example: success
        data:
          type: array
          items:
            '$ref': '#/components/schemas/CfdiSummary'
        meta:
          type: object
          properties:
            total:
              type: integer
            page:
              type: integer
            per_page:
              type: integer
            total_pages:
              type: integer
    CancellationRequest:
      type: object
      required:
        - motivo
      properties:
        motivo:
          type: string
          description: Motivo SAT (01-04). El 01 exige folio_sustitucion_uuid.
          enum:
            - '01'
            - '02'
            - '03'
            - '04'
          example: '02'
        folio_sustitucion_uuid:
          type: string
          nullable: true
          format: uuid
          description: UUID del CFDI que sustituye (obligatorio con motivo 01).
    CancellationStatusResponse:
      type: object
      properties:
        status:
          type: string
          example: success
        message:
          type: string
        data:
          type: object
          properties:
            uuid:
              type: string
              format: uuid
            cancel_status:
              type: string
              enum:
                - SOLICITADO_201
                - EN_PROCESO_ACEPTACION
                - CANCELADO
                - RECHAZADO
              description: Estado del flujo de cancelación SAT.
    EmisorCreateRequest:
      type: object
      required:
        - razon_social
        - rfc
        - regimen_fiscal
        - codigo_postal
      properties:
        external_ref:
          type: string
          nullable: true
          description: Referencia del tenant en el sistema del integrador.
          example: tenant-externo-42
        razon_social:
          type: string
          example: ESCUELA KEMPER URGATE
        rfc:
          type: string
          example: EKU9003173C9
        regimen_fiscal:
          type: string
          description: Clave del catálogo SAT c_RegimenFiscal.
          example: '601'
        codigo_postal:
          type: string
          example: '64000'
        curp:
          type: string
          nullable: true
    Emisor:
      type: object
      properties:
        rfc:
          type: string
        external_ref:
          type: string
          nullable: true
        razon_social:
          type: string
        regimen_fiscal:
          type: string
        codigo_postal:
          type: string
        is_principal:
          type: boolean
          description: true si el emisor es tu propia cuenta principal (registrada automáticamente al activar el API). Consume su propio saldo PREPAGO —la misma bolsa que financia el pool— y no puede desvincularse.
        status:
          type: string
          enum:
            - ACTIVO
            - SUSPENDIDO
        reused:
          type: boolean
          description: true si se vinculó una cuenta existente con el mismo RFC (respuesta 200); false si se creó un tenant nuevo (respuesta 201).
        csd:
          type: object
          nullable: true
          properties:
            cargado:
              type: boolean
            valid_to:
              type: string
              nullable: true
            serial:
              type: string
              nullable: true
    EmisorResponse:
      type: object
      properties:
        status:
          type: string
          example: success
        data:
          '$ref': '#/components/schemas/Emisor'
    CsdUploadRequest:
      type: object
      required:
        - cer_base64
        - key_base64
        - password
      properties:
        cer_base64:
          type: string
          description: Archivo .cer del CSD en base64.
        key_base64:
          type: string
          description: Archivo .key del CSD en base64.
        password:
          type: string
          description: Contraseña de la llave privada.
    CsdUploadResponse:
      type: object
      properties:
        status:
          type: string
          example: success
        message:
          type: string
        data:
          type: object
          properties:
            valid_from:
              type: string
            valid_to:
              type: string
            serial:
              type: string
    TimbresResponse:
      type: object
      properties:
        status:
          type: string
          example: success
        data:
          type: object
          properties:
            saldo:
              type: integer
            consumidos_mes_actual:
              type: integer
            por_emisor_mes_actual:
              type: array
              items:
                type: object
                properties:
                  emisor_rfc:
                    type: string
                  external_ref:
                    type: string
                    nullable: true
                  consumidos:
                    type: integer
    Comprobante:
      type: object
      description: Comprobante CFDI 4.0 canónico. Los totales e impuestos los calcula el integrador; Facter valida pero no recalcula. Sello/NoCertificado/Certificado se ignoran si vienen — los pone Facter.
      required:
        - Version
        - Moneda
        - SubTotal
        - Total
        - TipoDeComprobante
        - Exportacion
        - LugarExpedicion
        - Emisor
        - Receptor
        - Conceptos
      properties:
        Version:
          type: string
          enum:
            - '4.0'
          example: '4.0'
        Serie:
          type: string
          nullable: true
          example: A
        Folio:
          type: string
          nullable: true
          example: '123'
        FormaPago:
          type: string
          nullable: true
          description: Catálogo c_FormaPago. Omitir en comprobantes tipo P.
          example: '01'
        MetodoPago:
          type: string
          nullable: true
          enum:
            - PUE
            - PPD
          description: Omitir en comprobantes tipo P.
          example: PUE
        CondicionesDePago:
          type: string
          nullable: true
        SubTotal:
          type: string
          example: '1000.00'
        Descuento:
          type: string
          nullable: true
        Moneda:
          type: string
          description: Catálogo c_Moneda. XXX para comprobantes tipo P.
          example: MXN
        TipoCambio:
          type: string
          nullable: true
          description: Requerido cuando Moneda ≠ MXN/XXX.
        Total:
          type: string
          example: '1160.00'
        TipoDeComprobante:
          type: string
          enum:
            - I
            - E
            - P
          description: 'I=Ingreso, E=Egreso/NC, P=Pago.'
          example: I
        Exportacion:
          type: string
          enum:
            - '01'
            - '02'
            - '03'
          example: '01'
        LugarExpedicion:
          type: string
          description: Código postal del emisor.
          example: '64000'
        Emisor:
          '$ref': '#/components/schemas/CfdiEmisor'
        Receptor:
          '$ref': '#/components/schemas/CfdiReceptor'
        Conceptos:
          type: array
          items:
            '$ref': '#/components/schemas/Concepto'
        Impuestos:
          '$ref': '#/components/schemas/ImpuestosComprobante'
        CfdiRelacionados:
          type: array
          description: Array de grupos de CFDIs relacionados (uno por TipoRelacion). Requerido en tipo E (Nota de Crédito); null/omitido en los demás.
          items:
            '$ref': '#/components/schemas/CfdiRelacionadoGrupo'
        Complemento:
          allOf:
            - '$ref': '#/components/schemas/Complemento'
          description: Complemento.Pagos requerido en tipo P; null/omitido si no aplica.
        InformacionGlobal:
          allOf:
            - '$ref': '#/components/schemas/InformacionGlobal'
          description: Factura global (público en general); null/omitido si no aplica.
    CfdiEmisor:
      type: object
      required:
        - Rfc
        - Nombre
        - RegimenFiscal
      properties:
        Rfc:
          type: string
          example: EKU9003173C9
        Nombre:
          type: string
          example: ESCUELA KEMPER URGATE
        RegimenFiscal:
          type: string
          description: Catálogo c_RegimenFiscal.
          example: '601'
    CfdiReceptor:
      type: object
      required:
        - Rfc
        - Nombre
        - DomicilioFiscalReceptor
        - RegimenFiscalReceptor
        - UsoCFDI
      properties:
        Rfc:
          type: string
          example: XAXX010101000
        Nombre:
          type: string
          example: PUBLICO EN GENERAL
        DomicilioFiscalReceptor:
          type: string
          description: CP del receptor.
          example: '64000'
        RegimenFiscalReceptor:
          type: string
          example: '616'
        UsoCFDI:
          type: string
          description: Catálogo c_UsoCFDI.
          example: S01
    Concepto:
      type: object
      required:
        - ClaveProdServ
        - Cantidad
        - ClaveUnidad
        - Descripcion
        - ValorUnitario
        - Importe
        - ObjetoImp
      properties:
        ClaveProdServ:
          type: string
          example: '01010101'
        NoIdentificacion:
          type: string
          nullable: true
          example: SKU-01
        Cantidad:
          type: string
          example: '1.000000'
        ClaveUnidad:
          type: string
          example: H87
        Unidad:
          type: string
          nullable: true
          example: Pieza
        Descripcion:
          type: string
          example: Producto de prueba
        ValorUnitario:
          type: string
          example: '1000.00'
        Importe:
          type: string
          example: '1000.00'
        Descuento:
          type: string
          nullable: true
        ObjetoImp:
          type: string
          enum:
            - '01'
            - '02'
            - '03'
            - '04'
          description: 02 = objeto de impuesto.
          example: '02'
        Impuestos:
          '$ref': '#/components/schemas/ImpuestosConcepto'
    Traslado:
      type: object
      description: 'Impuesto trasladado. Si TipoFactor=Exento, enviar solo Base, Impuesto, TipoFactor (sin TasaOCuota ni Importe).'
      required:
        - Base
        - Impuesto
        - TipoFactor
      properties:
        Base:
          type: string
          example: '1000.00'
        Impuesto:
          type: string
          description: '001=ISR, 002=IVA, 003=IEPS.'
          example: '002'
        TipoFactor:
          type: string
          enum:
            - Tasa
            - Cuota
            - Exento
          example: Tasa
        TasaOCuota:
          type: string
          nullable: true
          example: '0.160000'
        Importe:
          type: string
          nullable: true
          example: '160.00'
    Retencion:
      type: object
      required:
        - Base
        - Impuesto
        - TipoFactor
        - TasaOCuota
        - Importe
      properties:
        Base:
          type: string
          example: '1000.00'
        Impuesto:
          type: string
          example: '001'
        TipoFactor:
          type: string
          enum:
            - Tasa
            - Cuota
          example: Tasa
        TasaOCuota:
          type: string
          example: '0.012500'
        Importe:
          type: string
          example: '12.50'
    ImpuestosConcepto:
      type: object
      properties:
        Traslados:
          type: array
          items:
            '$ref': '#/components/schemas/Traslado'
        Retenciones:
          type: array
          items:
            '$ref': '#/components/schemas/Retencion'
    ImpuestosComprobante:
      type: object
      description: Totales de impuestos a nivel comprobante.
      properties:
        TotalImpuestosTrasladados:
          type: string
          nullable: true
          example: '160.00'
        TotalImpuestosRetenidos:
          type: string
          nullable: true
        Traslados:
          type: array
          items:
            '$ref': '#/components/schemas/Traslado'
        Retenciones:
          type: array
          items:
            '$ref': '#/components/schemas/Retencion'
    CfdiRelacionadoGrupo:
      type: object
      description: Grupo de CFDIs relacionados que comparten un mismo TipoRelacion. TipoRelacion 07 exige FormaPago=30.
      required:
        - TipoRelacion
        - CfdiRelacionado
      properties:
        TipoRelacion:
          type: string
          enum:
            - '01'
            - '02'
            - '03'
            - '04'
            - '05'
            - '06'
            - '07'
          example: '01'
        CfdiRelacionado:
          type: array
          items:
            type: object
            required:
              - UUID
            properties:
              UUID:
                type: string
                format: uuid
                example: AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE
    InformacionGlobal:
      type: object
      description: Factura global al público en general.
      required:
        - Periodicidad
        - Meses
        - Año
      properties:
        Periodicidad:
          type: string
          enum:
            - '01'
            - '02'
            - '03'
            - '04'
            - '05'
          example: '04'
        Meses:
          type: string
          example: '06'
        'Año':
          type: string
          example: '2026'
    Complemento:
      type: object
      description: Complementos del CFDI. Para comprobantes tipo P se incluye Pagos (Complemento Pagos 2.0).
      properties:
        Pagos:
          '$ref': '#/components/schemas/Pagos20'
    Pagos20:
      type: object
      description: Complemento de Pagos 2.0. Requiere Totales y al menos un Pago con sus DoctosRelacionados.
      properties:
        Version:
          type: string
          enum:
            - '2.0'
          example: '2.0'
        Totales:
          type: object
          properties:
            MontoTotalPagos:
              type: string
              example: '1160.00'
        Pago:
          type: array
          items:
            '$ref': '#/components/schemas/Pago20'
    Pago20:
      type: object
      required:
        - FechaPago
        - FormaDePagoP
        - MonedaP
        - Monto
        - DoctosRelacionados
      properties:
        FechaPago:
          type: string
          example: '2026-06-10T12:00:00'
        FormaDePagoP:
          type: string
          example: '03'
        MonedaP:
          type: string
          example: MXN
        TipoCambioP:
          type: string
          nullable: true
        Monto:
          type: string
          example: '1160.00'
        DoctosRelacionados:
          type: array
          items:
            type: object
            properties:
              IdDocumento:
                type: string
                format: uuid
                description: UUID del CFDI de ingreso pagado.
              Serie:
                type: string
                nullable: true
              Folio:
                type: string
                nullable: true
              MonedaDR:
                type: string
                example: MXN
              EquivalenciaDR:
                type: string
                nullable: true
                description: 'Tipo de cambio del documento a la moneda del pago. Obligatorio y = "1" cuando MonedaDR = MonedaP (regla SAT CRP20238).'
                example: '1'
              NumParcialidad:
                type: string
                example: '1'
              ImpSaldoAnt:
                type: string
                example: '1160.00'
              ImpPagado:
                type: string
                example: '1160.00'
              ImpSaldoInsoluto:
                type: string
                example: '0.00'
              ObjetoImpDR:
                type: string
                example: '01'
