openapi: 3.1.0
info:
  title: API v1 · Sistema Facturación Perú
  version: 1.0.0
  summary: Emite y consulta comprobantes electrónicos, guías, clientes, productos y stock de tu empresa.
  description: |
    API REST de cada empresa en `https://<tu-subdominio>.sistemafacturacionperu.com/api/v1`.
    Modo pruebas (beta de SUNAT, empresa aparte, se borra a los 7 días): `https://pruebas.sistemafacturacionperu.com/api/v1`.

    - Autenticación: `Authorization: Bearer sfp_live_…` (o `sfp_test_…` en pruebas). Nunca en la URL (401 `clave_en_url`).
    - Éxito: `{ "success": true, "data": …, "meta"?: … }`. Error: `{ "success": false, "code": "…", "message": "…", "errors"?: {…} }`.
    - Todo `POST` acepta `Idempotency-Key` (recomendado): el reintento con el mismo cuerpo devuelve la misma respuesta
      con `Idempotent-Replayed: true`.
    - Límites: 120 llamadas por minuto por clave y 30 emisiones por minuto (cabeceras `X-RateLimit-Limit` y
      `X-RateLimit-Remaining`; al pasarte, 429 con `Retry-After`).
    - La API viene desde el Plan PRO (403 `plan_sin_api`).

    Guía completa: https://sistemafacturacionperu.com/desarrolladores/
  contact:
    name: Sistema Facturación Perú
    url: https://sistemafacturacionperu.com/desarrolladores
servers:
  - url: https://{subdominio}.sistemafacturacionperu.com/api/v1
    description: Producción (tu empresa)
    variables:
      subdominio:
        default: miempresa
  - url: https://pruebas.sistemafacturacionperu.com/api/v1
    description: Modo pruebas (beta de SUNAT, con una clave sfp_test_)
security:
  - bearer: []
  - conectar: []
tags:
  - name: Cuenta
  - name: Comprobantes
    description: Factura (01), boleta (03), nota de crédito (07) y de débito (08).
  - name: Anulaciones
  - name: Cobros
  - name: Guías
    description: Guía de remisión remitente (09) y transportista (31).
  - name: Otros documentos
    description: Retenciones (20), percepciones (40), liquidaciones de compra (04), notas de venta y cotizaciones.
  - name: Catálogo
  - name: Consultas
  - name: Webhooks

paths:
  /yo:
    get:
      tags: [Cuenta]
      summary: Con qué clave, usuario, local y permisos estás llamando
      operationId: yo
      responses:
        '200':
          description: La clave y su contexto
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RespuestaYo' }
              example:
                success: true
                data:
                  empresa: { ruc: '20601234567', razon_social: 'FERRETERIA LIMA S.A.C.', nombre_comercial: 'Ferre Lima' }
                  modo: produccion
                  sunat: produccion
                  clave: { id: 3, nombre: 'Tienda en línea', prefijo: 'sfp_live_Ab12Cd34', vence: null }
                  legacy: false
                  alcances: [comprobantes.emitir, comprobantes.leer]
                  usuario: { id: 1, nombre: 'Ana Torres', tipo: admin }
                  establecimiento: { id: 1, codigo: '0000', nombre: 'Tienda principal' }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '403': { $ref: '#/components/responses/Prohibido' }
        '423': { $ref: '#/components/responses/CuentaBloqueada' }
        '429': { $ref: '#/components/responses/DemasiadasSolicitudes' }

  /comprobantes:
    post:
      tags: [Comprobantes]
      summary: Emitir factura, boleta, nota de crédito o de débito
      description: |
        El mismo JSON de `POST /api/documents` de la API antigua, más `establecimiento`, `origen` y, en cada línea,
        `almacen_id`, `series` y `lotes` por código. Se envía a SUNAT según la configuración de la empresa (las boletas van
        en el resumen diario). Alcance `comprobantes.emitir`; función `sales.documents.create`.
      operationId: emitirComprobante
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Comprobante' }
            examples:
              boleta:
                summary: Boleta al contado
                value:
                  serie_documento: B001
                  numero_documento: '#'
                  fecha_de_emision: '2026-11-03'
                  hora_de_emision: '10:15:00'
                  codigo_tipo_operacion: '0101'
                  codigo_tipo_documento: '03'
                  codigo_tipo_moneda: PEN
                  establecimiento: '0000'
                  datos_del_cliente_o_receptor:
                    codigo_tipo_documento_identidad: '1'
                    numero_documento: '70123456'
                    apellidos_y_nombres_o_razon_social: 'Ana Pérez'
                    codigo_pais: PE
                  totales:
                    total_operaciones_gravadas: 50.85
                    total_igv: 9.15
                    total_impuestos: 9.15
                    total_valor: 50.85
                    total_venta: 60
                  items:
                    - codigo_interno: POLO-01
                      descripcion: Polo azul talla M
                      unidad_de_medida: NIU
                      cantidad: 2
                      valor_unitario: 25.42
                      codigo_tipo_precio: '01'
                      precio_unitario: 30
                      codigo_tipo_afectacion_igv: '10'
                      total_base_igv: 50.85
                      porcentaje_igv: 18
                      total_igv: 9.15
                      total_impuestos: 9.15
                      total_valor_item: 50.85
                      total_item: 60
                  pagos:
                    - codigo_metodo_pago: '01'
                      codigo_destino_pago: cash
                      monto: 60
              factura_desde_cotizacion:
                summary: Factura que convierte la cotización 15
                value:
                  serie_documento: F001
                  numero_documento: '#'
                  fecha_de_emision: '2026-11-03'
                  hora_de_emision: '10:15:00'
                  codigo_tipo_operacion: '0101'
                  codigo_tipo_documento: '01'
                  codigo_tipo_moneda: PEN
                  origen: { tipo: cotizacion, id: 15 }
                  datos_del_cliente_o_receptor:
                    codigo_tipo_documento_identidad: '6'
                    numero_documento: '20601234567'
                    apellidos_y_nombres_o_razon_social: 'CONSTRUCTORA ANDINA S.A.C.'
                    codigo_pais: PE
                    ubigeo: '150101'
                    direccion: 'Av. Lima 123'
                  totales: { total_operaciones_gravadas: 1000, total_igv: 180, total_impuestos: 180, total_valor: 1000, total_venta: 1180 }
                  items:
                    - { codigo_interno: CEM-42, descripcion: 'Cemento 42.5 kg', unidad_de_medida: NIU, cantidad: 40, valor_unitario: 25, codigo_tipo_precio: '01', precio_unitario: 29.5, codigo_tipo_afectacion_igv: '10', total_base_igv: 1000, porcentaje_igv: 18, total_igv: 180, total_impuestos: 180, total_valor_item: 1000, total_item: 1180, lotes: [{ codigo: 'L-2026-10', cantidad: 40 }] }
                  codigo_condicion_de_pago: '02'
                  cuotas: [{ fecha: '2026-12-03', codigo_tipo_moneda: PEN, monto: 1180 }]
      responses:
        '201':
          description: Emitido
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { $ref: '#/components/schemas/ComprobanteEmitido' }
              example:
                success: true
                data:
                  id: 1520
                  external_id: 6f1c7d0e-8f7e-4a1c-9d5b-2a2f8e1c9b10
                  tipo: '03'
                  serie: B001
                  numero: 845
                  numero_completo: B001-845
                  fecha_emision: '2026-11-03'
                  hora_emision: '10:15:00'
                  establecimiento_id: 1
                  moneda: PEN
                  total: 60
                  cliente: { tipo_documento: '1', numero_documento: '70123456', nombre: 'Ana Pérez' }
                  estado: { codigo: '01', clave: registrado, descripcion: Registrado }
                  sunat: null
                  hash: 'k2y5S1y3O2Q8+1u7p5h0Lw=='
                  enlaces:
                    pdf: https://miempresa.sistemafacturacionperu.com/api/v1/comprobantes/1520/pdf
                    pdf_ticket: https://miempresa.sistemafacturacionperu.com/api/v1/comprobantes/1520/pdf?formato=ticket
                    xml: https://miempresa.sistemafacturacionperu.com/api/v1/comprobantes/1520/xml
                    cdr: null
                  enlaces_publicos:
                    pdf: https://miempresa.sistemafacturacionperu.com/downloads/document/pdf/6f1c7d0e-8f7e-4a1c-9d5b-2a2f8e1c9b10
                    xml: https://miempresa.sistemafacturacionperu.com/downloads/document/xml/6f1c7d0e-8f7e-4a1c-9d5b-2a2f8e1c9b10
                    cdr: null
        '400': { $ref: '#/components/responses/PeticionInvalida' }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '403': { $ref: '#/components/responses/Prohibido' }
        '409': { $ref: '#/components/responses/Idempotencia' }
        '422': { $ref: '#/components/responses/ReglaDeNegocio' }
        '423': { $ref: '#/components/responses/CuentaBloqueada' }
        '429': { $ref: '#/components/responses/DemasiadasSolicitudes' }
        '500': { $ref: '#/components/responses/ErrorInterno' }
    get:
      tags: [Comprobantes]
      summary: Listar comprobantes (paginado)
      description: Un usuario que no ve toda la empresa (por ejemplo, un vendedor) ve solo los suyos. Alcance `comprobantes.leer`.
      operationId: listarComprobantes
      parameters:
        - { name: desde, in: query, schema: { type: string, format: date } }
        - { name: hasta, in: query, schema: { type: string, format: date } }
        - { name: tipo, in: query, schema: { type: string, enum: ['01', '03', '07', '08'] } }
        - { name: serie, in: query, schema: { type: string } }
        - { name: numero, in: query, schema: { type: integer } }
        - { name: estado, in: query, description: 'Clave (registrado, enviado, aceptado, observado, rechazado, anulado, por_anular) o código SUNAT', schema: { type: string } }
        - { name: cliente_numero, in: query, description: 'RUC o DNI del cliente', schema: { type: string } }
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Página de comprobantes
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { const: true }
                  data: { type: array, items: { $ref: '#/components/schemas/ComprobanteEmitido' } }
                  meta: { $ref: '#/components/schemas/Meta' }
        '401': { $ref: '#/components/responses/NoAutenticado' }
        '403': { $ref: '#/components/responses/Prohibido' }
        '422': { $ref: '#/components/responses/ReglaDeNegocio' }

  /comprobantes/buscar:
    get:
      tags: [Comprobantes]
      summary: Buscar por serie y número
      operationId: buscarComprobante
      parameters:
        - { name: serie, in: query, required: true, schema: { type: string }, example: F001 }
        - { name: numero, in: query, required: true, schema: { type: integer }, example: 120 }
        - { name: tipo, in: query, schema: { type: string, enum: ['01', '03', '07', '08'] } }
      responses:
        '200': { $ref: '#/components/responses/UnComprobante' }
        '404': { $ref: '#/components/responses/NoEncontrado' }

  /comprobantes/{id}:
    get:
      tags: [Comprobantes]
      summary: Consultar un comprobante (por id o external_id)
      operationId: verComprobante
      parameters: [{ $ref: '#/components/parameters/IdDocumento' }]
      responses:
        '200': { $ref: '#/components/responses/UnComprobante' }
        '404': { $ref: '#/components/responses/NoEncontrado' }

  /comprobantes/{id}/pdf:
    get:
      tags: [Comprobantes]
      summary: Descargar el PDF
      description: Alcance `comprobantes.leer`; función `sales.documents.export`.
      operationId: pdfComprobante
      parameters:
        - $ref: '#/components/parameters/IdDocumento'
        - { name: formato, in: query, schema: { type: string, enum: [a4, a5, ticket, ticket_58, ticket_50], default: a4 } }
      responses:
        '200': { description: PDF, content: { application/pdf: { schema: { type: string, format: binary } } } }
        '404': { $ref: '#/components/responses/NoEncontrado' }
        '503': { $ref: '#/components/responses/NoDisponible' }
  /comprobantes/{id}/xml:
    get:
      tags: [Comprobantes]
      summary: Descargar el XML firmado
      operationId: xmlComprobante
      parameters: [{ $ref: '#/components/parameters/IdDocumento' }]
      responses:
        '200': { description: XML firmado, content: { application/xml: { schema: { type: string, format: binary } } } }
        '404': { $ref: '#/components/responses/NoEncontrado' }
  /comprobantes/{id}/cdr:
    get:
      tags: [Comprobantes]
      summary: Descargar la constancia de SUNAT (CDR, zip)
      operationId: cdrComprobante
      parameters: [{ $ref: '#/components/parameters/IdDocumento' }]
      responses:
        '200': { description: CDR, content: { application/zip: { schema: { type: string, format: binary } } } }
        '404':
          description: Aún sin CDR (`cdr_no_disponible`) o no existe
          content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }

  /comprobantes/{id}/anular:
    post:
      tags: [Anulaciones]
      summary: Anular (el sistema elige baja o resumen)
      description: |
        Factura y sus notas → comunicación de baja; boleta y sus notas → resumen diario de anulación. Solo comprobantes
        aceptados u observados. Devuelve el ticket de SUNAT. Alcance `comprobantes.anular`; función `sales.documents.void`.
      operationId: anularComprobante
      parameters: [{ $ref: '#/components/parameters/IdDocumento' }, { $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [motivo], properties: { motivo: { type: string, minLength: 3, maxLength: 100 } } }
            example: { motivo: 'Error en el precio' }
      responses:
        '202':
          description: Anulación enviada; consulta el ticket
          content:
            application/json:
              schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Anulacion' } } }
              example:
                success: true
                data: { tipo: baja, ticket: '1730646000123', external_id: 'a1b2…', estado: { codigo: '03', clave: enviado, descripcion: Enviado }, consultar: 'https://miempresa.sistemafacturacionperu.com/api/v1/anulaciones/1730646000123' }
        '409': { $ref: '#/components/responses/Conflicto' }
        '422': { $ref: '#/components/responses/ReglaDeNegocio' }
  /anulaciones/{ticket}:
    get:
      tags: [Anulaciones]
      summary: Estado de una baja o resumen de anulación (consulta a SUNAT)
      operationId: estadoAnulacion
      parameters: [{ name: ticket, in: path, required: true, schema: { type: string } }]
      responses:
        '200':
          description: Estado
          content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Anulacion' } } } } }
        '404': { $ref: '#/components/responses/NoEncontrado' }

  /comprobantes/{id}/reenviar-sunat:
    post:
      tags: [Comprobantes]
      summary: Reenviar una factura (o su nota) registrada a SUNAT
      description: Las boletas van en el resumen diario (422 `va_en_resumen`). Función `sales.sunat_status.manage`.
      operationId: reenviarSunat
      parameters: [{ $ref: '#/components/parameters/IdDocumento' }, { $ref: '#/components/parameters/IdempotencyKey' }]
      responses:
        '200': { $ref: '#/components/responses/UnComprobante' }
        '409': { $ref: '#/components/responses/Conflicto' }
        '422': { $ref: '#/components/responses/ReglaDeNegocio' }

  /comprobantes/{id}/enviar:
    post:
      tags: [Comprobantes]
      summary: Enviar por correo o por el WhatsApp de tu empresa
      description: WhatsApp sale SOLO por la línea propia de tu empresa (Canales › WhatsApp), nunca por la de la plataforma.
      operationId: enviarComprobante
      parameters: [{ $ref: '#/components/parameters/IdDocumento' }, { $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [canal, destino]
              properties:
                canal: { type: string, enum: [correo, whatsapp] }
                destino: { type: string, description: 'Correo (hasta 5, separados por coma) o celular de 9 dígitos' }
                mensaje: { type: string, maxLength: 500 }
            examples:
              correo: { value: { canal: correo, destino: 'compras@cliente.pe' } }
              whatsapp: { value: { canal: whatsapp, destino: '987654321' } }
      responses:
        '200':
          description: Enviado
          content: { application/json: { example: { success: true, data: { enviado: true, canal: whatsapp, destino: '51987654321', mensaje: 'Enviado por WhatsApp a 987 654 321 (Mi línea).' } } } }
        '422': { $ref: '#/components/responses/ReglaDeNegocio' }
        '502': { description: 'El proveedor de correo o WhatsApp falló (`envio_fallido`)', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /comprobantes/{id}/cobros:
    get:
      tags: [Cobros]
      summary: Cobros y saldo pendiente
      operationId: verCobros
      parameters: [{ $ref: '#/components/parameters/IdDocumento' }]
      responses:
        '200':
          description: Saldo
          content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Saldo' } } } } }
    post:
      tags: [Cobros]
      summary: Registrar un cobro
      description: |
        Por el mismo servicio de la web (Por cobrar › Registrar cobro): un medio que no es efectivo va a una cuenta de cobro
        (`cuenta_cobro_id`, banco o billetera del tipo del medio) y con N.° de operación. Alcance `cobros.registrar`;
        función `finance.payments.create`.
      operationId: registrarCobro
      parameters: [{ $ref: '#/components/parameters/IdDocumento' }, { $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [fecha, monto, medio]
              properties:
                fecha: { type: string, format: date }
                monto: { type: number, exclusiveMinimum: 0 }
                medio: { type: string, description: 'Código del medio de pago (01 efectivo, 005 transferencia, …)' }
                cuenta_cobro_id: { type: integer, description: 'Cuenta de banco o billetera (sin ella: la caja)' }
                referencia: { type: string, description: 'N.° de operación' }
                fecha_operacion: { type: string, format: date }
                cuota_id: { type: integer }
            example: { fecha: '2026-11-03', monto: 590, medio: '005', cuenta_cobro_id: 2, referencia: 'OP-778812' }
      responses:
        '201':
          description: Cobro registrado y saldo
          content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { allOf: [{ $ref: '#/components/schemas/Saldo' }, { type: object, properties: { cobro_id: { type: integer } } }] } } } } }
        '422': { $ref: '#/components/responses/ReglaDeNegocio' }

  /guias:
    post:
      tags: [Guías]
      summary: Emitir guía (crea, envía y consulta en una llamada)
      description: |
        El JSON de `POST /api/dispatches`. Si SUNAT aún no responde el ticket, el estado es `pendiente`: consulta con
        `GET /guias/{id}`. Alcance `guias.emitir`; funciones `sales.dispatches.create` y `sales.dispatches.emit`.
      operationId: emitirGuia
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Guia' }
            example:
              serie_documento: T001
              numero_documento: '#'
              fecha_de_emision: '2026-11-03'
              hora_de_emision: '09:00:00'
              codigo_tipo_documento: '09'
              datos_del_cliente_o_receptor: { codigo_tipo_documento_identidad: '6', numero_documento: '20601234567', apellidos_y_nombres_o_razon_social: 'CONSTRUCTORA ANDINA S.A.C.' }
              codigo_modo_transporte: '02'
              codigo_motivo_traslado: '01'
              fecha_de_traslado: '2026-11-03'
              peso_total: 1000
              unidad_peso_total: KGM
              direccion_partida: { ubigeo: '150101', direccion: 'Av. Lima 123', codigo_del_domicilio_fiscal: '0000' }
              direccion_llegada: { ubigeo: '150132', direccion: 'Jr. Las Obras 456', codigo_del_domicilio_fiscal: '0000' }
              chofer: { codigo_tipo_documento_identidad: '1', numero_documento: '40123456', nombres: 'Luis Quispe', numero_licencia: 'Q40123456' }
              vehiculo: { numero_de_placa: ABC123 }
              items: [{ codigo_interno: CEM-42, descripcion: 'Cemento 42.5 kg', unidad_de_medida: NIU, cantidad: 40 }]
      responses:
        '201':
          description: Guía creada (aceptada o pendiente)
          content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/GuiaEmitida' } } } } }
        '422': { $ref: '#/components/responses/ReglaDeNegocio' }
    get:
      tags: [Guías]
      summary: Listar guías
      operationId: listarGuias
      parameters:
        - { name: desde, in: query, schema: { type: string, format: date } }
        - { name: hasta, in: query, schema: { type: string, format: date } }
        - { name: tipo, in: query, schema: { type: string, enum: ['09', '31'] } }
        - { name: serie, in: query, schema: { type: string } }
        - { name: numero, in: query, schema: { type: integer } }
        - { name: estado, in: query, schema: { type: string }, description: 'registrado, pendiente, aceptado, rechazado…' }
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Página de guías
          content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: array, items: { $ref: '#/components/schemas/GuiaEmitida' } }, meta: { $ref: '#/components/schemas/Meta' } } } } }
  /guias/{id}:
    get:
      tags: [Guías]
      summary: Consultar una guía (si está pendiente, consulta el ticket en SUNAT)
      operationId: verGuia
      parameters: [{ $ref: '#/components/parameters/IdDocumento' }]
      responses:
        '200': { description: La guía, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/GuiaEmitida' } } } } } }
        '404': { $ref: '#/components/responses/NoEncontrado' }
  /guias/{id}/enviar:
    post:
      tags: [Guías]
      summary: Reintentar el envío de una guía registrada
      operationId: enviarGuia
      parameters: [{ $ref: '#/components/parameters/IdDocumento' }, { $ref: '#/components/parameters/IdempotencyKey' }]
      responses:
        '200': { description: La guía, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/GuiaEmitida' } } } } } }
        '409': { $ref: '#/components/responses/Conflicto' }
  /guias/{id}/pdf:
    get:
      tags: [Guías]
      summary: PDF de la guía
      operationId: pdfGuia
      parameters: [{ $ref: '#/components/parameters/IdDocumento' }]
      responses: { '200': { description: PDF, content: { application/pdf: { schema: { type: string, format: binary } } } }, '404': { $ref: '#/components/responses/NoEncontrado' } }
  /guias/{id}/xml:
    get:
      tags: [Guías]
      summary: XML firmado de la guía
      operationId: xmlGuia
      parameters: [{ $ref: '#/components/parameters/IdDocumento' }]
      responses: { '200': { description: XML, content: { application/xml: { schema: { type: string, format: binary } } } }, '404': { $ref: '#/components/responses/NoEncontrado' } }
  /guias/{id}/cdr:
    get:
      tags: [Guías]
      summary: CDR de la guía
      operationId: cdrGuia
      parameters: [{ $ref: '#/components/parameters/IdDocumento' }]
      responses: { '200': { description: CDR, content: { application/zip: { schema: { type: string, format: binary } } } }, '404': { $ref: '#/components/responses/NoEncontrado' } }

  /retenciones:
    post:
      tags: [Otros documentos]
      summary: Emitir comprobante de retención (20)
      description: El JSON de `POST /api/retentions`. Función `sales.advanced_documents.create`.
      operationId: emitirRetencion
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/JsonApiAntigua' } } } }
      responses:
        '201': { $ref: '#/components/responses/OtroCpe' }
        '422': { $ref: '#/components/responses/ReglaDeNegocio' }
  /percepciones:
    post:
      tags: [Otros documentos]
      summary: Emitir comprobante de percepción (40)
      description: El JSON de `POST /api/perceptions`.
      operationId: emitirPercepcion
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/JsonApiAntigua' } } } }
      responses:
        '201': { $ref: '#/components/responses/OtroCpe' }
        '422': { $ref: '#/components/responses/ReglaDeNegocio' }
  /liquidaciones-compra:
    post:
      tags: [Otros documentos]
      summary: Emitir liquidación de compra (04)
      description: El JSON de `POST /api/purchase-settlements`.
      operationId: emitirLiquidacion
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/JsonApiAntigua' } } } }
      responses:
        '201': { $ref: '#/components/responses/OtroCpe' }
        '422': { $ref: '#/components/responses/ReglaDeNegocio' }
  /notas-venta:
    post:
      tags: [Otros documentos]
      summary: Registrar nota de venta
      description: |
        Con el JSON de los comprobantes (sin `codigo_tipo_documento`), `serie_documento` de notas de venta (NV01…) y
        `origen` cotización o pedido. Para facturarla después: `POST /comprobantes` con `origen: {tipo: nota_venta, id}`.
        Función `sales.sale_notes.create`.
      operationId: registrarNotaVenta
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Comprobante' } } } }
      responses:
        '201': { $ref: '#/components/responses/Borrador' }
        '422': { $ref: '#/components/responses/ReglaDeNegocio' }
  /cotizaciones:
    post:
      tags: [Otros documentos]
      summary: Registrar cotización
      description: Con el JSON de los comprobantes, sin serie; `fecha_de_vencimiento`, `fecha_de_entrega` y `observaciones` opcionales.
      operationId: registrarCotizacion
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/Comprobante' } } } }
      responses:
        '201': { $ref: '#/components/responses/Borrador' }
        '422': { $ref: '#/components/responses/ReglaDeNegocio' }

  /clientes:
    get:
      tags: [Catálogo]
      summary: Listar clientes
      operationId: listarClientes
      parameters: [{ name: buscar, in: query, schema: { type: string } }, { $ref: '#/components/parameters/Page' }, { $ref: '#/components/parameters/PerPage' }]
      responses:
        '200': { description: Página, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: array, items: { $ref: '#/components/schemas/Cliente' } }, meta: { $ref: '#/components/schemas/Meta' } } } } } }
    post:
      tags: [Catálogo]
      summary: Crear cliente
      operationId: crearCliente
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ClienteEntrada' }
            example: { tipo_documento: '6', numero_documento: '20601234567', nombre: 'BODEGA LIMA S.A.C.', direccion: 'Av. Lima 1', ubigeo: '150101', correo: 'compras@bodega.pe' }
      responses:
        '201': { description: Creado, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Cliente' } } } } } }
        '409': { $ref: '#/components/responses/Conflicto' }
        '422': { $ref: '#/components/responses/ReglaDeNegocio' }
  /clientes/{numero}:
    parameters: [{ name: numero, in: path, required: true, description: 'RUC, DNI u otro documento', schema: { type: string } }]
    get:
      tags: [Catálogo]
      summary: Consultar cliente por número de documento
      operationId: verCliente
      responses:
        '200': { description: Cliente, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Cliente' } } } } } }
        '404': { $ref: '#/components/responses/NoEncontrado' }
    put:
      tags: [Catálogo]
      summary: Cambiar datos del cliente (el documento no cambia)
      operationId: cambiarCliente
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ClienteEntrada' } } } }
      responses:
        '200': { description: Cliente, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Cliente' } } } } } }
  /productos:
    get:
      tags: [Catálogo]
      summary: Listar productos
      operationId: listarProductos
      parameters: [{ name: buscar, in: query, schema: { type: string } }, { $ref: '#/components/parameters/Page' }, { $ref: '#/components/parameters/PerPage' }]
      responses:
        '200': { description: Página, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: array, items: { $ref: '#/components/schemas/Producto' } }, meta: { $ref: '#/components/schemas/Meta' } } } } } }
    post:
      tags: [Catálogo]
      summary: Crear producto (nace sin stock)
      operationId: crearProducto
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ProductoEntrada' }
            example: { codigo_interno: CAF-01, descripcion: 'Café 250 g', precio_unitario: 18.5, unidad_de_medida: NIU, codigo_tipo_afectacion_igv: '10' }
      responses:
        '201': { description: Creado, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Producto' } } } } } }
        '409': { $ref: '#/components/responses/Conflicto' }
  /productos/{id}:
    parameters: [{ name: id, in: path, required: true, description: 'Id o código interno', schema: { type: string } }]
    get:
      tags: [Catálogo]
      summary: Consultar producto
      operationId: verProducto
      responses:
        '200': { description: Producto, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Producto' } } } } } }
        '404': { $ref: '#/components/responses/NoEncontrado' }
    put:
      tags: [Catálogo]
      summary: Cambiar producto (cambiar el precio pide catalog.items.prices.update)
      operationId: cambiarProducto
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ProductoEntrada' } } } }
      responses:
        '200': { description: Producto, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Producto' } } } } } }
  /stock:
    get:
      tags: [Catálogo]
      summary: Stock por almacén, lote y serie
      description: Alcance `stock.leer`; función `inventory.read`.
      operationId: verStock
      parameters:
        - { name: producto, in: query, description: 'Id o código interno', schema: { type: string } }
        - { name: almacen, in: query, schema: { type: integer } }
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Stock
          content:
            application/json:
              schema: { type: object, properties: { success: { const: true }, data: { type: array, items: { $ref: '#/components/schemas/Stock' } }, meta: { $ref: '#/components/schemas/Meta' } } }
              example:
                success: true
                data:
                  - producto_id: 12
                    codigo_interno: POLO-01
                    descripcion: Polo azul
                    control_de_stock: true
                    stock_total: 10
                    almacenes: [{ almacen_id: 1, almacen: 'Almacén Lima', establecimiento: '0000', stock: 7 }, { almacen_id: 2, almacen: 'Almacén Arequipa', establecimiento: '0001', stock: 3 }]
                    lotes: [{ codigo: L-01, cantidad: 5, vence: '2027-01-31', almacen_id: 1 }]
                    series: [{ numero: SN-1, almacen_id: 1 }]
                meta: { page: 1, per_page: 20, total: 1, last_page: 1 }
  /establecimientos:
    get:
      tags: [Catálogo]
      summary: Locales con sus series y almacenes
      operationId: listarEstablecimientos
      responses:
        '200':
          description: Locales
          content:
            application/json:
              example:
                success: true
                data:
                  - id: 1
                    codigo: '0000'
                    nombre: Tienda principal
                    direccion: 'Av. Lima 123'
                    ubigeo: '150101'
                    series: [{ tipo: '01', serie: F001 }, { tipo: '03', serie: B001 }, { tipo: '09', serie: T001 }]
                    almacenes: [{ id: 1, nombre: 'Almacén Lima', principal: true }]

  /consultas/ruc/{numero}:
    get:
      tags: [Consultas]
      summary: Consulta RUC (SUNAT)
      operationId: consultaRuc
      parameters: [{ name: numero, in: path, required: true, schema: { type: string, pattern: '^(10|15|16|17|20)\d{9}$' } }]
      responses:
        '200':
          description: Datos del RUC
          content: { application/json: { example: { success: true, data: { ruc: '20601234567', razon_social: 'BODEGA LIMA S.A.C.', nombre_comercial: null, direccion: 'AV. LIMA 1', ubigeo: '150101', estado: ACTIVO, condicion: HABIDO, agente_de_retencion: false } } } }
        '404': { $ref: '#/components/responses/NoEncontrado' }
        '502': { $ref: '#/components/responses/NoDisponible' }
  /consultas/dni/{numero}:
    get:
      tags: [Consultas]
      summary: Consulta DNI (RENIEC)
      operationId: consultaDni
      parameters: [{ name: numero, in: path, required: true, schema: { type: string, pattern: '^\d{8}$' } }]
      responses:
        '200': { description: Datos del DNI, content: { application/json: { example: { success: true, data: { dni: '70123456', nombre: 'PEREZ GOMEZ ANA', nombres: ANA, apellido_paterno: PEREZ, apellido_materno: GOMEZ, direccion: null, ubigeo: null } } } } }
        '404': { $ref: '#/components/responses/NoEncontrado' }
  /consultas/tipo-cambio:
    get:
      tags: [Consultas]
      summary: Tipo de cambio SUNAT
      operationId: tipoCambio
      parameters: [{ name: fecha, in: query, schema: { type: string, format: date } }]
      responses:
        '200': { description: Tipo de cambio, content: { application/json: { example: { success: true, data: { fecha: '2026-11-03', compra: 3.712, venta: 3.741 } } } } }

  /webhooks:
    get:
      tags: [Webhooks]
      summary: Listar webhooks
      operationId: listarWebhooks
      responses:
        '200': { description: Webhooks y eventos disponibles, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: array, items: { $ref: '#/components/schemas/Webhook' } }, meta: { type: object, properties: { eventos_disponibles: { type: object } } } } } } } }
    post:
      tags: [Webhooks]
      summary: Crear webhook (el secreto se muestra una vez)
      operationId: crearWebhook
      parameters: [{ $ref: '#/components/parameters/IdempotencyKey' }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, eventos]
              properties:
                url: { type: string, format: uri, description: 'https y dominio público' }
                eventos: { type: array, items: { $ref: '#/components/schemas/Evento' } }
                descripcion: { type: string }
            example: { url: 'https://mi-sistema.pe/avisos-sfp', eventos: [comprobante.aceptado, comprobante.rechazado] }
      responses:
        '201':
          description: Creado
          content: { application/json: { example: { success: true, data: { id: 4, url: 'https://mi-sistema.pe/avisos-sfp', eventos: [comprobante.aceptado, comprobante.rechazado], activo: true, secreto: 'whsec_9fK2…', aviso: 'Guarda el secreto ahora: no se vuelve a mostrar.' } } } }
        '422': { $ref: '#/components/responses/ReglaDeNegocio' }
  /webhooks/{id}:
    delete:
      tags: [Webhooks]
      summary: Borrar webhook
      operationId: borrarWebhook
      parameters: [{ name: id, in: path, required: true, schema: { type: integer } }]
      responses:
        '200': { description: Borrado, content: { application/json: { example: { success: true, data: { borrado: true, id: 4 } } } } }
        '404': { $ref: '#/components/responses/NoEncontrado' }
  /webhooks/{id}/entregas:
    get:
      tags: [Webhooks]
      summary: Entregas de un webhook
      operationId: entregasWebhook
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
        - { name: estado, in: query, schema: { type: string, enum: [pendiente, reintentando, entregado, fallido] } }
        - $ref: '#/components/parameters/Page'
      responses:
        '200': { description: Entregas, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { type: array, items: { $ref: '#/components/schemas/Entrega' } }, meta: { $ref: '#/components/schemas/Meta' } } } } } }
  /webhooks/entregas/{id}/reenviar:
    post:
      tags: [Webhooks]
      summary: Reenviar una entrega
      operationId: reenviarEntrega
      parameters: [{ name: id, in: path, required: true, schema: { type: integer } }, { $ref: '#/components/parameters/IdempotencyKey' }]
      responses:
        '202': { description: En cola, content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/Entrega' } } } } } }

webhooks:
  aviso:
    post:
      summary: Aviso de un cambio de estado SUNAT
      description: |
        Cabeceras: `X-SFP-Signature: t=<unix>,v1=<hex HMAC-SHA256(secreto, "t.cuerpo")>`, `X-SFP-Delivery: <uuid>`,
        `X-SFP-Event: <evento>`. Responde 2xx en menos de 10 s; si no, reintentamos a los 1 m, 5 m, 30 m, 2 h, 6 h y 24 h.
        No seguimos redirecciones.
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/Aviso' }
            example:
              id: 0b8f3c1e-2d4a-4f7e-9a61-6c5d2e1f0a9b
              evento: comprobante.aceptado
              creado: '2026-11-03T10:15:09-05:00'
              data: { tipo_documento: comprobante, id: 1520, external_id: 6f1c7d0e-8f7e-4a1c-9d5b-2a2f8e1c9b10, tipo: '01', numero_completo: F001-120, fecha_emision: '2026-11-03', total: 1180, moneda: PEN, estado: { codigo: '05', clave: aceptado, descripcion: Aceptado } }
      responses:
        '200': { description: Recibido }

components:
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: 'Clave `sfp_live_<40>` (o `sfp_test_<40>` en pruebas). El token antiguo del usuario también vale (todos los alcances).'
    conectar:
      type: oauth2
      description: |
        «Conectar con Sistema Facturación Perú» (https://sistemafacturacionperu.com/desarrolladores/conectar/): el cliente de tu sistema conecta su cuenta con
        un botón. El token de acceso `sfp_oat_<48>` va en `Authorization: Bearer` y vale 1 hora, solo en la API de SU
        empresa (`empresa.api_url` de la respuesta del token). PKCE S256 y `state` obligatorios. En el Plan Básico solo
        sirven los alcances de emitir: lo demás responde 403 `plan_insuficiente` (con `errors.plan`).
      flows:
        authorizationCode:
          authorizationUrl: https://api.sistemafacturacionperu.com/conectar
          tokenUrl: https://api.sistemafacturacionperu.com/oauth/token
          refreshUrl: https://api.sistemafacturacionperu.com/oauth/token
          scopes:
            comprobantes.emitir: Emitir facturas, boletas, notas, notas de venta, cotizaciones, retenciones, percepciones y liquidaciones de compra
            comprobantes.leer: Ver tus comprobantes y descargar su PDF, XML y CDR
            comprobantes.anular: Anular comprobantes
            guias.emitir: Emitir guías de remisión
            guias.leer: Ver tus guías de remisión y descargar sus archivos
            cobros.registrar: Registrar cobros (desde PRO)
            catalogo.leer: Ver clientes, productos y locales (desde PRO)
            catalogo.escribir: Crear y cambiar clientes y productos (desde PRO)
            stock.leer: Ver el stock (desde PRO)
            consultas: Consultar RUC, DNI y tipo de cambio
            webhooks.gestionar: Crear y borrar webhooks (desde PRO)
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: 'Hasta 255 caracteres visibles (un UUID). Se recuerda 24 h.'
      schema: { type: string, maxLength: 255 }
    IdDocumento:
      name: id
      in: path
      required: true
      description: Id numérico o external_id (UUID)
      schema: { type: string }
    Page: { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
    PerPage: { name: per_page, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
  responses:
    UnComprobante:
      description: El comprobante
      content: { application/json: { schema: { type: object, properties: { success: { const: true }, data: { $ref: '#/components/schemas/ComprobanteEmitido' } } } } }
    OtroCpe:
      description: Emitido
      content: { application/json: { example: { success: true, data: { id: 33, external_id: 'c3…', tipo: '20', numero_completo: R001-33, fecha_emision: '2026-11-03', estado: { codigo: '05', clave: aceptado, descripcion: Aceptado }, sunat: { codigo: '0', descripcion: 'El Comprobante numero R001-33, ha sido aceptado', notas: [] }, enlaces_publicos: { pdf: '…', xml: '…', cdr: '…' } } } } }
    Borrador:
      description: Registrado
      content: { application/json: { example: { success: true, data: { id: 88, external_id: 'd4…', tipo: '80', numero_completo: NV01-88, fecha_emision: '2026-11-03', moneda: PEN, total: 60, estado: { codigo: '01', clave: registrado, descripcion: Registrado }, enlaces_publicos: { pdf: '…/sale-notes/print/d4…/a4', pdf_ticket: '…/ticket' } } } } }
    PeticionInvalida:
      description: JSON inválido (`json_invalido`) o Idempotency-Key mal formada (`idempotencia_invalida`)
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    NoAutenticado:
      description: '`clave_invalida`, `clave_revocada`, `clave_vencida`, `clave_en_url`, `clave_de_pruebas`'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { success: false, code: clave_revocada, message: 'Esta clave fue revocada. Crea otra en Configuración › Sistema › Integraciones › API.' } } }
    Prohibido:
      description: '`scope_faltante` (con la cabecera X-SFP-Scope-Required), `sin_permiso`, `plan_sin_api`, `establecimiento_no_permitido`'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { success: false, code: scope_faltante, message: 'Esta clave no tiene el permiso «comprobantes.emitir» (Emitir facturas, boletas…). Crea una clave con ese permiso.' } } }
    NoEncontrado:
      description: '`no_encontrado`'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Conflicto:
      description: '`duplicado`, `ya_anulado`, `no_reenviable`, `conflicto`'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Idempotencia:
      description: '`idempotencia_distinta` (otra petición con la misma clave) o `idempotencia_en_proceso`'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    ReglaDeNegocio:
      description: Validación (`validacion`, con `errors` por campo) o una regla del negocio con su código (ver el catálogo de la guía).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            validacion: { value: { success: false, code: validacion, message: 'Revisa los datos enviados.', errors: { 'totales.total_venta': ['El campo totales.total venta es obligatorio.'] } } }
            detraccion: { value: { success: false, code: detraccion_minimo, message: 'Sin detracción: el código 037 aplica a ventas mayores a S/ 700.00, y esta es de S/ 600.00. …', errors: { detraccion: ['…'] } } }
            limite: { value: { success: false, code: limite_plan, message: 'Ha superado el límite permitido para la emisión de comprobantes' } }
    CuentaBloqueada:
      description: '`cuenta_bloqueada`: pago vencido o cuenta suspendida'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    DemasiadasSolicitudes:
      description: '`demasiadas_solicitudes` o `demasiadas_emisiones`, con Retry-After'
      headers:
        Retry-After: { schema: { type: integer } }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    NoDisponible:
      description: '`archivo_no_disponible`, `consulta_no_disponible`, `no_disponible`'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    ErrorInterno:
      description: '`error_interno` (sin detalles; la clave de idempotencia queda libre para reintentar)'
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
  schemas:
    Error:
      type: object
      required: [success, code, message]
      properties:
        success: { const: false }
        code: { type: string, description: Código estable (ver el catálogo de errores) }
        message: { type: string, description: 'En español, para mostrar' }
        errors: { type: object, additionalProperties: { type: array, items: { type: string } } }
    Meta:
      type: object
      properties: { page: { type: integer }, per_page: { type: integer }, total: { type: integer }, last_page: { type: integer } }
    Estado:
      type: object
      properties:
        codigo: { type: string, description: 'state_type_id: 01 registrado, 03 enviado, 05 aceptado, 07 observado, 09 rechazado, 11 anulado, 13 por anular' }
        clave: { type: string, enum: [registrado, enviado, pendiente, aceptado, observado, rechazado, anulado, por_anular, desconocido] }
        descripcion: { type: string }
    Sunat:
      type: [object, 'null']
      properties: { codigo: { type: [string, 'null'] }, descripcion: { type: [string, 'null'] }, notas: { type: array, items: { type: string } } }
    RespuestaYo:
      type: object
      properties:
        success: { const: true }
        data:
          type: object
          properties:
            empresa: { type: object }
            modo: { type: string, enum: [produccion, pruebas] }
            sunat: { type: string, enum: [produccion, beta, interno] }
            clave: { type: object }
            legacy: { type: boolean }
            alcances: { type: array, items: { $ref: '#/components/schemas/Alcance' } }
            usuario: { type: object }
            establecimiento: { type: [object, 'null'] }
    Alcance:
      type: string
      enum: [comprobantes.emitir, comprobantes.leer, comprobantes.anular, guias.emitir, guias.leer, cobros.registrar, catalogo.leer, catalogo.escribir, stock.leer, consultas, webhooks.gestionar]
    Origen:
      type: object
      required: [tipo, id]
      properties:
        tipo: { type: string, enum: [cotizacion, pedido, nota_venta, guia] }
        id: { type: integer }
    Linea:
      type: object
      required: [descripcion, unidad_de_medida, cantidad, valor_unitario, precio_unitario, codigo_tipo_afectacion_igv, total_item]
      properties:
        codigo_interno: { type: string, description: 'Con control de stock, el producto debe existir (422 producto_no_existe)' }
        descripcion: { type: string }
        unidad_de_medida: { type: string, example: NIU }
        cantidad: { type: number }
        valor_unitario: { type: number }
        codigo_tipo_precio: { type: string, example: '01' }
        precio_unitario: { type: number }
        codigo_tipo_afectacion_igv: { type: string, example: '10' }
        total_base_igv: { type: number }
        porcentaje_igv: { type: number }
        total_igv: { type: number }
        total_impuestos: { type: number }
        total_valor_item: { type: number }
        total_item: { type: number }
        almacen_id: { type: integer, description: Almacén del local del que sale }
        series: { type: array, items: { type: string }, description: Números de serie (tantas como la cantidad) }
        lotes: { type: array, items: { type: object, required: [codigo, cantidad], properties: { codigo: { type: string }, cantidad: { type: number } } } }
        actualizar_descripcion: { type: boolean, default: true }
    Comprobante:
      type: object
      required: [serie_documento, numero_documento, fecha_de_emision, hora_de_emision, codigo_tipo_documento, codigo_tipo_moneda, datos_del_cliente_o_receptor, totales, items]
      properties:
        serie_documento: { type: string, example: F001 }
        numero_documento: { type: string, description: "'#' = el siguiente correlativo" }
        fecha_de_emision: { type: string, format: date }
        hora_de_emision: { type: string, example: '10:15:00' }
        codigo_tipo_documento: { type: string, enum: ['01', '03', '07', '08'] }
        codigo_tipo_operacion: { type: string, description: '0101 venta interna; 1001 sujeta a detracción; 1004 transporte de carga…' }
        codigo_tipo_moneda: { type: string, example: PEN }
        factor_tipo_de_cambio: { type: number }
        fecha_de_vencimiento: { type: string, format: date }
        establecimiento: { type: string, description: 'Código SUNAT del local (GET /establecimientos)' }
        origen: { $ref: '#/components/schemas/Origen' }
        datos_del_cliente_o_receptor:
          type: object
          required: [codigo_tipo_documento_identidad, numero_documento, apellidos_y_nombres_o_razon_social]
          properties:
            codigo_tipo_documento_identidad: { type: string, description: '1 DNI, 6 RUC, 4 carné de extranjería, 7 pasaporte, 0 otros' }
            numero_documento: { type: string }
            apellidos_y_nombres_o_razon_social: { type: string }
            codigo_pais: { type: string }
            ubigeo: { type: string }
            direccion: { type: string }
            correo_electronico: { type: string }
            telefono: { type: string }
        totales:
          type: object
          required: [total_venta]
          properties:
            total_anticipos: { type: number }
            total_descuentos: { type: number }
            total_cargos: { type: number }
            total_exportacion: { type: number }
            total_operaciones_gravadas: { type: number }
            total_operaciones_inafectas: { type: number }
            total_operaciones_exoneradas: { type: number }
            total_operaciones_gratuitas: { type: number }
            total_igv: { type: number }
            total_impuestos_bolsa_plastica: { type: number }
            total_impuestos: { type: number }
            total_valor: { type: number }
            subtotal_venta: { type: number }
            total_venta: { type: number }
        items: { type: array, minItems: 1, items: { $ref: '#/components/schemas/Linea' } }
        pagos: { type: array, items: { type: object, required: [codigo_metodo_pago, codigo_destino_pago, monto], properties: { codigo_metodo_pago: { type: string }, codigo_destino_pago: { type: [string, integer], description: "'cash' o el id de la cuenta de cobro" }, referencia: { type: string }, monto: { type: number } } } }
        codigo_condicion_de_pago: { type: string, description: '01 contado, 02 crédito' }
        cuotas: { type: array, items: { type: object, required: [fecha, codigo_tipo_moneda, monto], properties: { fecha: { type: string, format: date }, codigo_tipo_moneda: { type: string }, monto: { type: number }, codigo_metodo_de_pago: { type: string } } } }
        detraccion: { type: object, description: 'Mínimo por código y sin retención (422 detraccion_minimo, detraccion_con_retencion, detraccion_operacion, detraccion_codigo)', properties: { codigo_tipo_detraccion: { type: string }, porcentaje: { type: number }, monto: { type: number }, codigo_metodo_pago: { type: string }, cuenta_bancaria: { type: string } } }
        retencion: { type: object, properties: { codigo: { type: string }, porcentaje: { type: number }, monto: { type: number }, base: { type: number } } }
        percepcion: { type: object, properties: { codigo: { type: string }, porcentaje: { type: number }, monto: { type: number }, base: { type: number } } }
        anticipos: { type: array, description: 'Cada anticipo debe tener saldo (422 anticipo_sin_saldo)', items: { type: object, properties: { numero: { type: string, example: F001-25 }, codigo_tipo_documento: { type: string }, monto: { type: number }, total: { type: number } } } }
        guias: { type: array, items: { type: object, properties: { numero: { type: string }, codigo_tipo_documento: { type: string } } } }
        documento_afectado: { type: object, description: 'Notas: external_id, o serie_documento + numero_documento + codigo_tipo_documento' }
        codigo_tipo_nota: { type: string }
        motivo_o_sustento_de_nota: { type: string }
        acciones: { type: object, properties: { enviar_email: { type: boolean }, enviar_xml_firmado: { type: boolean }, formato_pdf: { type: string } } }
        informacion_adicional: { type: string }
    ComprobanteEmitido:
      type: object
      properties:
        id: { type: integer }
        external_id: { type: string, format: uuid }
        tipo: { type: string }
        serie: { type: string }
        numero: { type: integer }
        numero_completo: { type: string }
        fecha_emision: { type: string, format: date }
        hora_emision: { type: string }
        establecimiento_id: { type: integer }
        moneda: { type: string }
        total: { type: number }
        cliente: { type: object, properties: { tipo_documento: { type: string }, numero_documento: { type: string }, nombre: { type: string } } }
        estado: { $ref: '#/components/schemas/Estado' }
        sunat: { $ref: '#/components/schemas/Sunat' }
        hash: { type: [string, 'null'] }
        enlaces: { type: object, properties: { pdf: { type: string }, pdf_ticket: { type: string }, xml: { type: string }, cdr: { type: [string, 'null'] } } }
        enlaces_publicos: { type: object, description: 'Descargas públicas por external_id (para tu cliente)' }
        avisos: { type: array, items: { type: string } }
    Anulacion:
      type: object
      properties:
        tipo: { type: string, enum: [baja, resumen] }
        ticket: { type: string }
        external_id: { type: string }
        estado: { $ref: '#/components/schemas/Estado' }
        sunat: { $ref: '#/components/schemas/Sunat' }
        comprobante: { $ref: '#/components/schemas/ComprobanteEmitido' }
        consultar: { type: string }
    Saldo:
      type: object
      properties:
        comprobante_id: { type: integer }
        numero_completo: { type: string }
        moneda: { type: string }
        total: { type: number }
        notas_de_credito: { type: number }
        cobrado: { type: number }
        por_confirmar: { type: number }
        saldo_pendiente: { type: number }
        cobros: { type: array, items: { type: object, properties: { id: { type: integer }, fecha: { type: string }, monto: { type: number }, medio: { type: string }, referencia: { type: [string, 'null'] }, confirmado: { type: boolean } } } }
    Guia:
      type: object
      description: El JSON de POST /api/dispatches (API antigua) + `establecimiento`, `origen` (cotizacion o pedido) y líneas con `almacen_id`.
      required: [serie_documento, numero_documento, fecha_de_emision, hora_de_emision, codigo_tipo_documento, fecha_de_traslado, peso_total, unidad_peso_total, direccion_partida, direccion_llegada, items]
      properties:
        codigo_tipo_documento: { type: string, enum: ['09', '31'] }
        establecimiento: { type: string }
        origen: { $ref: '#/components/schemas/Origen' }
        items: { type: array, items: { type: object, required: [codigo_interno, descripcion, cantidad], properties: { codigo_interno: { type: string }, descripcion: { type: string }, cantidad: { type: number }, almacen_id: { type: integer } } } }
      additionalProperties: true
    GuiaEmitida:
      type: object
      properties:
        id: { type: integer }
        external_id: { type: string }
        tipo: { type: string }
        serie: { type: string }
        numero: { type: integer }
        numero_completo: { type: string }
        fecha_emision: { type: string }
        fecha_traslado: { type: [string, 'null'] }
        estado: { $ref: '#/components/schemas/Estado' }
        ticket: { type: [string, 'null'] }
        sunat: { $ref: '#/components/schemas/Sunat' }
        enlaces: { type: object }
        avisos: { type: array, items: { type: string } }
    JsonApiAntigua:
      type: object
      description: El mismo JSON documentado en la API antigua para este comprobante (+ `establecimiento`).
      additionalProperties: true
    Cliente:
      type: object
      properties:
        id: { type: integer }
        tipo_documento: { type: string }
        numero_documento: { type: string }
        nombre: { type: string }
        nombre_comercial: { type: [string, 'null'] }
        direccion: { type: [string, 'null'] }
        ubigeo: { type: [string, 'null'] }
        codigo_pais: { type: [string, 'null'] }
        correo: { type: [string, 'null'] }
        telefono: { type: [string, 'null'] }
        activo: { type: boolean }
    ClienteEntrada:
      type: object
      properties:
        tipo_documento: { type: string, enum: ['0', '1', '4', '6', '7', A, B] }
        numero_documento: { type: string }
        nombre: { type: string }
        nombre_comercial: { type: string }
        direccion: { type: string }
        ubigeo: { type: string }
        codigo_pais: { type: string }
        correo: { type: string, format: email }
        telefono: { type: string }
    Producto:
      type: object
      properties:
        id: { type: integer }
        codigo_interno: { type: string }
        descripcion: { type: string }
        nombre: { type: [string, 'null'] }
        unidad_de_medida: { type: string }
        tipo: { type: string }
        moneda: { type: string }
        precio_unitario: { type: number }
        codigo_tipo_afectacion_igv: { type: string }
        incluye_igv: { type: boolean }
        codigo_producto_sunat: { type: [string, 'null'] }
        stock: { type: number }
        lotes: { type: boolean }
        series: { type: boolean }
        activo: { type: boolean }
    ProductoEntrada:
      type: object
      properties:
        codigo_interno: { type: string }
        descripcion: { type: string }
        nombre: { type: string }
        unidad_de_medida: { type: string }
        tipo: { type: string, enum: ['01', '02'] }
        moneda: { type: string, enum: [PEN, USD] }
        precio_unitario: { type: number }
        codigo_tipo_afectacion_igv: { type: string }
        incluye_igv: { type: boolean }
        codigo_producto_sunat: { type: string }
        activo: { type: boolean }
    Stock:
      type: object
      properties:
        producto_id: { type: integer }
        codigo_interno: { type: string }
        descripcion: { type: string }
        control_de_stock: { type: boolean }
        stock_total: { type: number }
        almacenes: { type: array, items: { type: object, properties: { almacen_id: { type: integer }, almacen: { type: string }, establecimiento: { type: [string, 'null'] }, stock: { type: number } } } }
        lotes: { type: array, items: { type: object, properties: { codigo: { type: string }, cantidad: { type: number }, vence: { type: [string, 'null'] }, almacen_id: { type: [integer, 'null'] } } } }
        series: { type: array, items: { type: object, properties: { numero: { type: string }, almacen_id: { type: [integer, 'null'] } } } }
    Evento:
      type: string
      enum: [comprobante.aceptado, comprobante.observado, comprobante.rechazado, comprobante.anulado, guia.aceptada, guia.rechazada, resumen.procesado]
    Webhook:
      type: object
      properties:
        id: { type: integer }
        url: { type: string }
        eventos: { type: array, items: { $ref: '#/components/schemas/Evento' } }
        descripcion: { type: [string, 'null'] }
        activo: { type: boolean }
        ultima_entrega: { type: [object, 'null'] }
        creado: { type: string }
    Entrega:
      type: object
      properties:
        id: { type: integer }
        uuid: { type: string }
        webhook_id: { type: integer }
        evento: { $ref: '#/components/schemas/Evento' }
        estado: { type: string, enum: [pendiente, reintentando, entregado, fallido] }
        intentos: { type: integer }
        codigo_respuesta: { type: [integer, 'null'] }
        error: { type: [string, 'null'] }
        proximo_intento: { type: [string, 'null'] }
        entregado: { type: [string, 'null'] }
        creado: { type: string }
    Aviso:
      type: object
      properties:
        id: { type: string, description: 'Igual a X-SFP-Delivery: úsalo para no procesar dos veces' }
        evento: { $ref: '#/components/schemas/Evento' }
        creado: { type: string, format: date-time }
        data: { type: object }
