# GENERADO POR tools/api/generar_contrato.php — NO EDITAR A MANO.
# Las rutas, los filtros y sus valores salen de ApiConsulta::catalogo()
# y los permisos de ApiLlave::permisos(). Si cambias el código, vuelve
# a generar; si editas esto, el próximo generado te lo borra.
openapi: 3.0.3
info:
  title: API de Wizerp
  version: '1.0.0'
  description: |-
    API de servidor a servidor de Wizerp. Todo va en JSON y todo está acotado a tu empresa.

    La empresa sale de tu llave: no es un parámetro, no se puede pasar y no hay forma
    de leer datos de otra.

    Esta API no se llama desde el navegador. Una petición con la cabecera Origin,
    con ?sid= o con x-csrf-mexerp se rechaza con 400.
servers:
  - url: 'https://api.wizerp.com/api/v1'
    description: Producción
security:
  - bearer: []
    keyId: []
tags:
  - name: Servicio
  - name: Prospectos
  - name: Clientes
  - name: Productos
  - name: Proveedores
  - name: Existencias
  - name: Almacenes
  - name: Ventas
  - name: Renglones de venta
  - name: Facturas (CFDI)
paths:
  /health:
    get:
      tags: [Servicio]
      summary: Comprobar que la API responde
      description: |-
        No necesita llave y no toca la base de datos: dice que el proceso está vivo y sirviendo, no que la base conteste.
      operationId: comprobarSalud
      security: []
      responses:
        '200':
          description: La API responde
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  version: { type: string }
                  peticion: { type: string }
        '404': { $ref: '#/components/responses/e404' }
        '405': { $ref: '#/components/responses/e405' }
        '500': { $ref: '#/components/responses/e500' }
  /leads:
    post:
      tags: [Prospectos]
      summary: Registrar un prospecto
      operationId: registrarProspecto
      description: |-
        Crea una ficha de prospecto en el CRM. Requiere el permiso leads:write.

        Necesita nombre o empresa, y correo o teléfono. Los datos van en el cuerpo JSON, nunca en la URL.

        Manda la cabecera Idempotency-Key para poder reintentar sin duplicar: con la misma clave y los mismos datos se devuelve la respuesta guardada, byte por byte.
      parameters:
        - { name: Idempotency-Key, in: header, required: false, schema: { type: string, maxLength: 255 }, description: Para reintentar sin duplicar }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                nombre: { type: string, maxLength: 100 }
                empresa: { type: string, maxLength: 200 }
                correo: { type: string, maxLength: 50 }
                telefono: { type: string, maxLength: 30 }
                mensaje: { type: string, maxLength: 2000 }
                vendedor: { type: integer }
            examples:
              sitio:
                summary: Formulario de contacto de un sitio
                value:
                  nombre: María Fernanda Olvera
                  empresa: Distribuidora Olvera
                  correo: mf.olvera@ejemplo.com.mx
                  telefono: '33 1234 5678'
                  mensaje: Facturamos 400 veces al mes
      responses:
        '201':
          description: Prospecto creado
        '200':
          description: Reintento idempotente; devuelve la respuesta guardada
        '400':
          $ref: '#/components/responses/e400'
        '401':
          $ref: '#/components/responses/e401'
        '403':
          $ref: '#/components/responses/e403'
        '404':
          $ref: '#/components/responses/e404'
        '409':
          $ref: '#/components/responses/e409'
        '413':
          $ref: '#/components/responses/e413'
        '422':
          $ref: '#/components/responses/e422'
        '429':
          $ref: '#/components/responses/e429'
        '500':
          $ref: '#/components/responses/e500'
        '503':
          $ref: '#/components/responses/e503'
  /clientes:
    get:
      tags: [Clientes]
      summary: Lista de clientes
      operationId: listarClientes
      description: |-
        Devuelve los registros de tu empresa, paginados por cursor. Requiere el permiso clientes:read.

        Las consultas devuelven páginas, no listas completas. Pide la siguiente pasando
        en `cursor` el valor que vino en `siguiente`, hasta que `hay_mas` sea false.

        No hay conteo total: contar millones de filas cuesta lo mismo que traerlas.

        Sólo se aceptan los filtros documentados. Cualquier otro parámetro contesta
        400 filtro_no_permitido; no se ignora en silencio, porque un filtro ignorado
        hace creer que filtraste.

        El campo correo puede traer varias direcciones separadas por coma y recortadas: el filtro correo compara el campo completo, así que no encuentra a un cliente por su segunda dirección.
      parameters:
        - { name: cursor, in: query, required: false, schema: { type: string }, description: La clave que vino en siguiente de la página anterior }
        - { name: limite, in: query, required: false, schema: { type: integer, minimum: 1, maximum: 500, default: 100 } }
        - name: clave
          in: query
          required: false
          schema: { type: integer }
          description: Número entero.
        - name: folio
          in: query
          required: false
          schema: { type: string }
          description: Coincidencia exacta. No admite comodines.
        - name: correo
          in: query
          required: false
          schema: { type: string }
          description: 'Correo exacto. Se compara normalizado: no distingue mayúsculas ni espacios de sobra.'
        - name: telefono
          in: query
          required: false
          schema: { type: string }
          description: 'Se comparan los últimos 10 caracteres tras quitar espacios, guiones, paréntesis y el signo de más. Manda el número como lo tengas.'
        - name: movil
          in: query
          required: false
          schema: { type: string }
          description: 'Se comparan los últimos 10 caracteres tras quitar espacios, guiones, paréntesis y el signo de más. Manda el número como lo tengas.'
        - name: rfc
          in: query
          required: false
          schema: { type: string }
          description: Coincidencia exacta. No admite comodines.
        - name: prospecto
          in: query
          required: false
          schema: { type: boolean }
          description: true o false.
        - name: publico
          in: query
          required: false
          schema: { type: boolean }
          description: true o false.
        - name: id_mercadolibre
          in: query
          required: false
          schema: { type: string }
          description: Coincidencia exacta. No admite comodines.
        - name: id_shopify
          in: query
          required: false
          schema: { type: string }
          description: Coincidencia exacta. No admite comodines.
        - name: id_woocommerce
          in: query
          required: false
          schema: { type: string }
          description: Coincidencia exacta. No admite comodines.
        - name: id_tiendanube
          in: query
          required: false
          schema: { type: string }
          description: Coincidencia exacta. No admite comodines.
        - name: id_walmart
          in: query
          required: false
          schema: { type: string }
          description: Coincidencia exacta. No admite comodines.
        - name: uso_cfdi
          in: query
          required: false
          schema: { type: string }
          description: 'La clave del SAT, por ejemplo G03. No el identificador interno de Wizerp.'
        - name: modificado_desde
          in: query
          required: false
          schema: { type: string }
          description: 'Fecha ISO 8601 en UTC, por ejemplo 2026-10-01T00:00:00Z. Devuelve lo que sea igual o posterior.'
        - name: proximo_contacto_desde
          in: query
          required: false
          schema: { type: string }
          description: 'Fecha ISO 8601 en UTC, por ejemplo 2026-10-01T00:00:00Z. Devuelve lo que sea igual o posterior.'
        - name: proximo_contacto_hasta
          in: query
          required: false
          schema: { type: string }
          description: Fecha ISO 8601 en UTC. Devuelve lo que sea igual o anterior.
      responses:
        '200':
          description: Una página de resultados
          content:
            application/json:
              schema:
                type: object
                required: [ok, datos, hay_mas]
                properties:
                  ok: { type: boolean }
                  datos:
                    type: array
                    items:
                      type: object
                      properties:
                        clave: { type: integer }
                        folio: { type: string, nullable: true }
                        nombre: { type: string, nullable: true }
                        razon_social: { type: string, nullable: true }
                        rfc: { type: string, nullable: true }
                        correo: { type: string, nullable: true }
                        telefono: { type: string, nullable: true }
                        movil: { type: string, nullable: true }
                        prospecto: { type: boolean }
                        id_mercadolibre: { type: string, nullable: true }
                        id_shopify: { type: string, nullable: true }
                        id_woocommerce: { type: string, nullable: true }
                        id_tiendanube: { type: string, nullable: true }
                        id_walmart: { type: string, nullable: true }
                        publico: { type: boolean }
                        proximo_contacto: { type: string, nullable: true }
                        modificado: { type: string, format: date-time, nullable: true }
                  siguiente: { type: string, nullable: true, description: Pásalo en cursor para la página siguiente }
                  hay_mas: { type: boolean }
        '400':
          $ref: '#/components/responses/e400'
        '401':
          $ref: '#/components/responses/e401'
        '403':
          $ref: '#/components/responses/e403'
        '404':
          $ref: '#/components/responses/e404'
        '429':
          $ref: '#/components/responses/e429'
        '500':
          $ref: '#/components/responses/e500'
        '503':
          $ref: '#/components/responses/e503'
  /productos:
    get:
      tags: [Productos]
      summary: Lista de productos
      operationId: listarProductos
      description: |-
        Devuelve los registros de tu empresa, paginados por cursor. Requiere el permiso productos:read.

        Las consultas devuelven páginas, no listas completas. Pide la siguiente pasando
        en `cursor` el valor que vino en `siguiente`, hasta que `hay_mas` sea false.

        No hay conteo total: contar millones de filas cuesta lo mismo que traerlas.

        Sólo se aceptan los filtros documentados. Cualquier otro parámetro contesta
        400 filtro_no_permitido; no se ignora en silencio, porque un filtro ignorado
        hace creer que filtraste.

        Los productos eliminados no se devuelven salvo que pidas estatus=eliminado.
      parameters:
        - { name: cursor, in: query, required: false, schema: { type: string }, description: La clave que vino en siguiente de la página anterior }
        - { name: limite, in: query, required: false, schema: { type: integer, minimum: 1, maximum: 500, default: 100 } }
        - name: clave
          in: query
          required: false
          schema: { type: integer }
          description: Número entero.
        - name: sku
          in: query
          required: false
          schema: { type: string }
          description: Coincidencia exacta. No admite comodines.
        - name: folio
          in: query
          required: false
          schema: { type: integer }
          description: Número entero.
        - name: estatus
          in: query
          required: false
          schema: { type: string, enum: [activo, inactivo, eliminado] }
          description: Uno de los valores de la lista.
        - name: tipo
          in: query
          required: false
          schema: { type: string, enum: [normal, virtual_con_precio, virtual_sin_precio, gasto] }
          description: Uno de los valores de la lista.
        - name: subtipo
          in: query
          required: false
          schema: { type: string, enum: [terminado, componente, materia_prima] }
          description: Uno de los valores de la lista.
        - name: kit
          in: query
          required: false
          schema: { type: boolean }
          description: true o false.
        - name: codigo_sat
          in: query
          required: false
          schema: { type: string }
          description: Coincidencia exacta. No admite comodines.
        - name: id_shopify
          in: query
          required: false
          schema: { type: string }
          description: Coincidencia exacta. No admite comodines.
        - name: marca
          in: query
          required: false
          schema: { type: string }
          description: 'El nombre tal como aparece en el catálogo, no su identificador.'
        - name: categoria
          in: query
          required: false
          schema: { type: string }
          description: 'El nombre tal como aparece en el catálogo, no su identificador.'
        - name: sku_padre
          in: query
          required: false
          schema: { type: string }
          description: 'El código del producto. Si el SKU corresponde a varios productos, se devuelven las filas de todos.'
        - name: buscar
          in: query
          required: false
          schema: { type: string }
          description: Búsqueda de texto. Mínimo 3 caracteres en al menos una palabra; las más cortas se descartan. No admite comodines.
        - name: modificado_desde
          in: query
          required: false
          schema: { type: string }
          description: 'Fecha ISO 8601 en UTC, por ejemplo 2026-10-01T00:00:00Z. Devuelve lo que sea igual o posterior.'
      responses:
        '200':
          description: Una página de resultados
          content:
            application/json:
              schema:
                type: object
                required: [ok, datos, hay_mas]
                properties:
                  ok: { type: boolean }
                  datos:
                    type: array
                    items:
                      type: object
                      properties:
                        clave: { type: integer }
                        sku: { type: string, nullable: true }
                        folio: { type: string, nullable: true }
                        descripcion: { type: string, nullable: true }
                        estatus: { type: string, enum: [activo, inactivo, eliminado, desconocido] }
                        tipo: { type: string, enum: [normal, virtual_con_precio, virtual_sin_precio, gasto, desconocido] }
                        subtipo: { type: string, enum: [terminado, componente, materia_prima, desconocido] }
                        kit: { type: boolean }
                        clave_padre: { type: integer }
                        codigo_sat: { type: string, nullable: true }
                        marca: { type: string, nullable: true }
                        categoria: { type: string, nullable: true }
                        id_shopify: { type: string, nullable: true }
                        modificado: { type: string, format: date-time, nullable: true }
                  siguiente: { type: string, nullable: true, description: Pásalo en cursor para la página siguiente }
                  hay_mas: { type: boolean }
        '400':
          $ref: '#/components/responses/e400'
        '401':
          $ref: '#/components/responses/e401'
        '403':
          $ref: '#/components/responses/e403'
        '404':
          $ref: '#/components/responses/e404'
        '429':
          $ref: '#/components/responses/e429'
        '500':
          $ref: '#/components/responses/e500'
        '503':
          $ref: '#/components/responses/e503'
  /proveedores:
    get:
      tags: [Proveedores]
      summary: Lista de proveedores
      operationId: listarProveedores
      description: |-
        Devuelve los registros de tu empresa, paginados por cursor. Requiere el permiso proveedores:read.

        Las consultas devuelven páginas, no listas completas. Pide la siguiente pasando
        en `cursor` el valor que vino en `siguiente`, hasta que `hay_mas` sea false.

        No hay conteo total: contar millones de filas cuesta lo mismo que traerlas.

        Sólo se aceptan los filtros documentados. Cualquier otro parámetro contesta
        400 filtro_no_permitido; no se ignora en silencio, porque un filtro ignorado
        hace creer que filtraste.

        Los registros eliminados no se devuelven salvo que pidas estatus=eliminado.
      parameters:
        - { name: cursor, in: query, required: false, schema: { type: string }, description: La clave que vino en siguiente de la página anterior }
        - { name: limite, in: query, required: false, schema: { type: integer, minimum: 1, maximum: 500, default: 100 } }
        - name: clave
          in: query
          required: false
          schema: { type: integer }
          description: Número entero.
        - name: folio
          in: query
          required: false
          schema: { type: string }
          description: Coincidencia exacta. No admite comodines.
        - name: estatus
          in: query
          required: false
          schema: { type: string, enum: [activo, inactivo, eliminado] }
          description: Uno de los valores de la lista.
        - name: rfc
          in: query
          required: false
          schema: { type: string }
          description: Coincidencia exacta. No admite comodines.
        - name: nombre
          in: query
          required: false
          schema: { type: string }
          description: Búsqueda de texto. Mínimo 3 caracteres en al menos una palabra; las más cortas se descartan. No admite comodines.
      responses:
        '200':
          description: Una página de resultados
          content:
            application/json:
              schema:
                type: object
                required: [ok, datos, hay_mas]
                properties:
                  ok: { type: boolean }
                  datos:
                    type: array
                    items:
                      type: object
                      properties:
                        clave: { type: integer }
                        folio: { type: string, nullable: true }
                        nombre: { type: string, nullable: true }
                        contacto: { type: string, nullable: true }
                        rfc: { type: string, nullable: true }
                        correo: { type: string, nullable: true }
                        telefono: { type: string, nullable: true }
                        estatus: { type: string, enum: [activo, inactivo, eliminado, desconocido] }
                  siguiente: { type: string, nullable: true, description: Pásalo en cursor para la página siguiente }
                  hay_mas: { type: boolean }
        '400':
          $ref: '#/components/responses/e400'
        '401':
          $ref: '#/components/responses/e401'
        '403':
          $ref: '#/components/responses/e403'
        '404':
          $ref: '#/components/responses/e404'
        '429':
          $ref: '#/components/responses/e429'
        '500':
          $ref: '#/components/responses/e500'
        '503':
          $ref: '#/components/responses/e503'
  /inventario:
    get:
      tags: [Existencias]
      summary: Lista de existencias
      operationId: listarInventario
      description: |-
        Devuelve los registros de tu empresa, paginados por cursor. Requiere el permiso inventario:read.

        Las consultas devuelven páginas, no listas completas. Pide la siguiente pasando
        en `cursor` el valor que vino en `siguiente`, hasta que `hay_mas` sea false.

        No hay conteo total: contar millones de filas cuesta lo mismo que traerlas.

        Sólo se aceptan los filtros documentados. Cualquier otro parámetro contesta
        400 filtro_no_permitido; no se ignora en silencio, porque un filtro ignorado
        hace creer que filtraste.

        Puede devolver existencias de productos que ya no están activos: una existencia es un hecho sobre el almacén, no sobre el ciclo de vida del producto. Si sincronizas catálogo, cruza contra GET /productos.

        El campo apartado viene con signo y el ERP calcula el disponible como existencia menos el valor absoluto de apartado, con piso en minimo. No restes de frente o puedes ofrecer lo que Wizerp no vende.
      parameters:
        - { name: cursor, in: query, required: false, schema: { type: string }, description: La clave que vino en siguiente de la página anterior }
        - { name: limite, in: query, required: false, schema: { type: integer, minimum: 1, maximum: 500, default: 100 } }
        - name: clave
          in: query
          required: false
          schema: { type: integer }
          description: Número entero.
        - name: sku
          in: query
          required: false
          schema: { type: string }
          description: 'El código del producto. Si el SKU corresponde a varios productos, se devuelven las filas de todos.'
        - name: almacen
          in: query
          required: false
          schema: { type: string }
          description: El código de tres caracteres del almacén. Consúltalos en GET /almacenes.
        - name: sucursal
          in: query
          required: false
          schema: { type: integer }
          description: Número entero.
        - name: modificado_desde
          in: query
          required: false
          schema: { type: string }
          description: 'Fecha ISO 8601 en UTC, por ejemplo 2026-10-01T00:00:00Z. Devuelve lo que sea igual o posterior.'
      responses:
        '200':
          description: Una página de resultados
          content:
            application/json:
              schema:
                type: object
                required: [ok, datos, hay_mas]
                properties:
                  ok: { type: boolean }
                  datos:
                    type: array
                    items:
                      type: object
                      properties:
                        clave: { type: integer }
                        sku: { type: string, nullable: true }
                        descripcion: { type: string, nullable: true }
                        almacen: { type: string, nullable: true }
                        sucursal: { type: integer }
                        existencia: { type: number }
                        apartado: { type: number }
                        por_embarcar: { type: number }
                        minimo: { type: number }
                        ubicacion: { type: string, nullable: true }
                        modificado: { type: string, format: date-time, nullable: true }
                  siguiente: { type: string, nullable: true, description: Pásalo en cursor para la página siguiente }
                  hay_mas: { type: boolean }
        '400':
          $ref: '#/components/responses/e400'
        '401':
          $ref: '#/components/responses/e401'
        '403':
          $ref: '#/components/responses/e403'
        '404':
          $ref: '#/components/responses/e404'
        '429':
          $ref: '#/components/responses/e429'
        '500':
          $ref: '#/components/responses/e500'
        '503':
          $ref: '#/components/responses/e503'
  /almacenes:
    get:
      tags: [Almacenes]
      summary: Lista de almacenes
      operationId: listarAlmacenes
      description: |-
        Devuelve los registros de tu empresa, paginados por cursor. Requiere el permiso inventario:read.

        Las consultas devuelven páginas, no listas completas. Pide la siguiente pasando
        en `cursor` el valor que vino en `siguiente`, hasta que `hay_mas` sea false.

        No hay conteo total: contar millones de filas cuesta lo mismo que traerlas.

        Sólo se aceptan los filtros documentados. Cualquier otro parámetro contesta
        400 filtro_no_permitido; no se ignora en silencio, porque un filtro ignorado
        hace creer que filtraste.

        Los registros eliminados no se devuelven salvo que pidas estatus=eliminado.
      parameters:
        - { name: cursor, in: query, required: false, schema: { type: string }, description: La clave que vino en siguiente de la página anterior }
        - { name: limite, in: query, required: false, schema: { type: integer, minimum: 1, maximum: 500, default: 100 } }
        - name: clave
          in: query
          required: false
          schema: { type: integer }
          description: Número entero.
        - name: codigo
          in: query
          required: false
          schema: { type: string }
          description: Coincidencia exacta. No admite comodines.
        - name: sucursal
          in: query
          required: false
          schema: { type: integer }
          description: Número entero.
        - name: principal
          in: query
          required: false
          schema: { type: boolean }
          description: true o false.
        - name: estatus
          in: query
          required: false
          schema: { type: string, enum: [activo, inactivo, eliminado] }
          description: Uno de los valores de la lista.
      responses:
        '200':
          description: Una página de resultados
          content:
            application/json:
              schema:
                type: object
                required: [ok, datos, hay_mas]
                properties:
                  ok: { type: boolean }
                  datos:
                    type: array
                    items:
                      type: object
                      properties:
                        clave: { type: integer }
                        codigo: { type: string, nullable: true }
                        nombre: { type: string, nullable: true }
                        folio: { type: string, nullable: true }
                        sucursal: { type: integer }
                        principal: { type: boolean }
                        estatus: { type: string, enum: [activo, inactivo, eliminado, desconocido] }
                        poblacion: { type: string, nullable: true }
                        estado: { type: string, nullable: true }
                  siguiente: { type: string, nullable: true, description: Pásalo en cursor para la página siguiente }
                  hay_mas: { type: boolean }
        '400':
          $ref: '#/components/responses/e400'
        '401':
          $ref: '#/components/responses/e401'
        '403':
          $ref: '#/components/responses/e403'
        '404':
          $ref: '#/components/responses/e404'
        '429':
          $ref: '#/components/responses/e429'
        '500':
          $ref: '#/components/responses/e500'
        '503':
          $ref: '#/components/responses/e503'
  /ventas:
    get:
      tags: [Ventas]
      summary: Lista de ventas
      operationId: listarVentas
      description: |-
        Devuelve los registros de tu empresa, paginados por cursor. Requiere el permiso ventas:read.

        Las consultas devuelven páginas, no listas completas. Pide la siguiente pasando
        en `cursor` el valor que vino en `siguiente`, hasta que `hay_mas` sea false.

        No hay conteo total: contar millones de filas cuesta lo mismo que traerlas.

        Sólo se aceptan los filtros documentados. Cualquier otro parámetro contesta
        400 filtro_no_permitido; no se ignora en silencio, porque un filtro ignorado
        hace creer que filtraste.

        Aquí viven TODOS los documentos de venta: remisiones, facturas, cotizaciones, pedidos y anticipos. Sin el filtro tipo se devuelven las ventas (tipo=venta; remision es sinónimo). Para los demás pide ese tipo: cada tipo se pagina por separado, no hay un listado de todos a la vez.

        En pedidos, surtido_estatus y monto_surtido traen el avance del surtido; en los demás tipos vienen null. No hay filtro por surtido.

        Los documentos cancelados no salen salvo que pidas cancelada=true.

        El filtro folio busca en el folio del tipo consultado: el mismo número puede existir como venta y como factura.

        Si filtras por fechas, el rango máximo es de 92 días: para un periodo mayor, pide por ventanas y recorre cada una con cursor.

        Los renglones de un documento se consultan en GET /ventas-renglones con la clave que devuelve esta lista.
      parameters:
        - { name: cursor, in: query, required: false, schema: { type: string }, description: La clave que vino en siguiente de la página anterior }
        - { name: limite, in: query, required: false, schema: { type: integer, minimum: 1, maximum: 500, default: 100 } }
        - name: clave
          in: query
          required: false
          schema: { type: integer }
          description: Número entero.
        - name: tipo
          in: query
          required: false
          schema: { type: string, enum: [remision, venta, factura, cotizacion, pedido, anticipo] }
          description: Uno de los valores de la lista.
        - name: folio
          in: query
          required: false
          schema: { type: integer }
          description: 'El número de folio del documento. El folio se busca según el «tipo» consultado: el mismo número puede existir como venta y como factura.'
        - name: cliente
          in: query
          required: false
          schema: { type: integer }
          description: Número entero.
        - name: cancelada
          in: query
          required: false
          schema: { type: boolean }
          description: 'true devuelve solo documentos cancelados; false, solo vigentes. Sin este filtro, los cancelados no salen.'
        - name: fecha_desde
          in: query
          required: false
          schema: { type: string }
          description: 'Fecha ISO 8601 en UTC, por ejemplo 2026-10-01T00:00:00Z. Devuelve lo que sea igual o posterior.'
        - name: fecha_hasta
          in: query
          required: false
          schema: { type: string }
          description: Fecha ISO 8601 en UTC. Devuelve lo que sea igual o anterior.
        - name: sucursal
          in: query
          required: false
          schema: { type: integer }
          description: Número entero.
        - name: sku
          in: query
          required: false
          schema: { type: string }
          description: 'El código del producto. Devuelve los documentos que incluyen ese SKU en sus renglones. Si el SKU corresponde a varios productos, cuentan todos.'
      responses:
        '200':
          description: Una página de resultados
          content:
            application/json:
              schema:
                type: object
                required: [ok, datos, hay_mas]
                properties:
                  ok: { type: boolean }
                  datos:
                    type: array
                    items:
                      type: object
                      properties:
                        clave: { type: integer }
                        tipo: { type: string, enum: [remision, venta, factura, cotizacion, pedido, anticipo, desconocido] }
                        folio: { type: integer }
                        cancelada: { type: boolean }
                        fecha: { type: string, format: date-time, nullable: true }
                        cliente: { type: integer }
                        cliente_nombre: { type: string, nullable: true }
                        rfc: { type: string, nullable: true }
                        subtotal: { type: number }
                        descuento: { type: number }
                        iva: { type: number }
                        total: { type: number }
                        saldo: { type: number }
                        moneda: { type: string, nullable: true }
                        tipo_cambio: { type: number }
                        sucursal: { type: integer }
                        almacen: { type: integer }
                        vendedor: { type: integer }
                        uuid: { type: string, nullable: true }
                        creado: { type: string, format: date-time, nullable: true }
                        surtido_estatus: { type: string, enum: [activo, surtido_total, surtido_parcial, cancelado_parcial, desconocido] }
                        monto_surtido: { type: number }
                  siguiente: { type: string, nullable: true, description: Pásalo en cursor para la página siguiente }
                  hay_mas: { type: boolean }
        '400':
          $ref: '#/components/responses/e400'
        '401':
          $ref: '#/components/responses/e401'
        '403':
          $ref: '#/components/responses/e403'
        '404':
          $ref: '#/components/responses/e404'
        '429':
          $ref: '#/components/responses/e429'
        '500':
          $ref: '#/components/responses/e500'
        '503':
          $ref: '#/components/responses/e503'
  /ventas-renglones:
    get:
      tags: [Renglones de venta]
      summary: Renglones de una venta
      operationId: listarVentasRenglones
      description: |-
        Devuelve los registros de tu empresa, paginados por cursor. Requiere el permiso ventas:read.

        Las consultas devuelven páginas, no listas completas. Pide la siguiente pasando
        en `cursor` el valor que vino en `siguiente`, hasta que `hay_mas` sea false.

        No hay conteo total: contar millones de filas cuesta lo mismo que traerlas.

        Sólo se aceptan los filtros documentados. Cualquier otro parámetro contesta
        400 filtro_no_permitido; no se ignora en silencio, porque un filtro ignorado
        hace creer que filtraste.

        El filtro venta es obligatorio: esta consulta devuelve los renglones de UN documento, no un listado general. La clave de la venta la da GET /ventas.

        El campo sku puede venir null si el producto fue eliminado por completo del catálogo.

        En renglones de un pedido, cantidad es lo pedido y cantidad_surtida lo ya surtido; en los demás documentos cantidad_surtida trae un eco de la cantidad o cero y no significa nada.
      parameters:
        - { name: cursor, in: query, required: false, schema: { type: string }, description: La clave que vino en siguiente de la página anterior }
        - { name: limite, in: query, required: false, schema: { type: integer, minimum: 1, maximum: 500, default: 100 } }
        - name: clave
          in: query
          required: true
          schema: { type: integer }
          description: Obligatorio. Número entero.
        - name: venta
          in: query
          required: true
          schema: { type: integer }
          description: Obligatorio. Número entero.
        - name: sku
          in: query
          required: false
          schema: { type: string }
          description: 'El código del producto. Si el SKU corresponde a varios productos, se devuelven las filas de todos.'
      responses:
        '200':
          description: Una página de resultados
          content:
            application/json:
              schema:
                type: object
                required: [ok, datos, hay_mas]
                properties:
                  ok: { type: boolean }
                  datos:
                    type: array
                    items:
                      type: object
                      properties:
                        clave: { type: integer }
                        venta: { type: integer }
                        sku: { type: string, nullable: true }
                        descripcion: { type: string, nullable: true }
                        unidad: { type: string, nullable: true }
                        cantidad: { type: number }
                        cantidad_surtida: { type: number }
                        precio: { type: number }
                        importe: { type: number }
                        descuento: { type: number }
                        total: { type: number }
                        almacen: { type: integer }
                        sucursal: { type: integer }
                  siguiente: { type: string, nullable: true, description: Pásalo en cursor para la página siguiente }
                  hay_mas: { type: boolean }
        '400':
          $ref: '#/components/responses/e400'
        '401':
          $ref: '#/components/responses/e401'
        '403':
          $ref: '#/components/responses/e403'
        '404':
          $ref: '#/components/responses/e404'
        '429':
          $ref: '#/components/responses/e429'
        '500':
          $ref: '#/components/responses/e500'
        '503':
          $ref: '#/components/responses/e503'
  /facturas:
    get:
      tags: [Facturas (CFDI)]
      summary: Lista de facturas timbradas
      operationId: listarFacturas
      description: |-
        Devuelve los registros de tu empresa, paginados por cursor. Requiere el permiso facturas:read.

        Las consultas devuelven páginas, no listas completas. Pide la siguiente pasando
        en `cursor` el valor que vino en `siguiente`, hasta que `hay_mas` sea false.

        No hay conteo total: contar millones de filas cuesta lo mismo que traerlas.

        Sólo se aceptan los filtros documentados. Cualquier otro parámetro contesta
        400 filtro_no_permitido; no se ignora en silencio, porque un filtro ignorado
        hace creer que filtraste.

        A diferencia de /ventas, esta lista trae TODOS los estatus —también canceladas y con error— porque es el registro de timbrado y su historia completa es el punto.

        Este es el registro de timbrado: aquí no hay importes. Los montos del documento están en GET /ventas (el campo venta de esta lista es la clave para consultarlo con tipo=factura).

        Si filtras por fechas, el rango máximo es de 92 días: para un periodo mayor, pide por ventanas y recorre cada una con cursor.
      parameters:
        - { name: cursor, in: query, required: false, schema: { type: string }, description: La clave que vino en siguiente de la página anterior }
        - { name: limite, in: query, required: false, schema: { type: integer, minimum: 1, maximum: 500, default: 100 } }
        - name: clave
          in: query
          required: false
          schema: { type: integer }
          description: Número entero.
        - name: uuid
          in: query
          required: false
          schema: { type: string }
          description: 'El folio fiscal (UUID) completo, 36 caracteres. No distingue mayúsculas.'
        - name: venta
          in: query
          required: false
          schema: { type: integer }
          description: Número entero.
        - name: tipo
          in: query
          required: false
          schema: { type: string, enum: [factura, nota_credito, pago, traslado, embarque, externa] }
          description: Uno de los valores de la lista.
        - name: estatus
          in: query
          required: false
          schema: { type: string, enum: [vigente, cancelada, en_espera, error] }
          description: Uno de los valores de la lista.
        - name: fecha_desde
          in: query
          required: false
          schema: { type: string }
          description: 'Fecha ISO 8601 en UTC, por ejemplo 2026-10-01T00:00:00Z. Devuelve lo que sea igual o posterior.'
        - name: fecha_hasta
          in: query
          required: false
          schema: { type: string }
          description: Fecha ISO 8601 en UTC. Devuelve lo que sea igual o anterior.
      responses:
        '200':
          description: Una página de resultados
          content:
            application/json:
              schema:
                type: object
                required: [ok, datos, hay_mas]
                properties:
                  ok: { type: boolean }
                  datos:
                    type: array
                    items:
                      type: object
                      properties:
                        clave: { type: integer }
                        uuid: { type: string, nullable: true }
                        tipo: { type: string, enum: [factura, nota_credito, pago, traslado, embarque, externa, desconocido] }
                        estatus: { type: string, enum: [vigente, cancelada, en_espera, error, desconocido] }
                        folio: { type: string, nullable: true }
                        venta: { type: integer }
                        cliente: { type: integer }
                        fecha: { type: string, format: date-time, nullable: true }
                        fecha_timbrado: { type: string, format: date-time, nullable: true }
                        uuid_relacionado: { type: string, nullable: true }
                  siguiente: { type: string, nullable: true, description: Pásalo en cursor para la página siguiente }
                  hay_mas: { type: boolean }
        '400':
          $ref: '#/components/responses/e400'
        '401':
          $ref: '#/components/responses/e401'
        '403':
          $ref: '#/components/responses/e403'
        '404':
          $ref: '#/components/responses/e404'
        '429':
          $ref: '#/components/responses/e429'
        '500':
          $ref: '#/components/responses/e500'
        '503':
          $ref: '#/components/responses/e503'
components:
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: 'El secreto completo de la llave, con el prefijo wzk_live_'
    keyId:
      type: apiKey
      in: header
      name: X-Wizerp-Key-Id
      description: |-
        El identificador público de la llave. Van LAS DOS: sin esta cabecera la petición falla con 401 aunque el secreto sea correcto.
  responses:
    e400:
      description: 'Petición mal formada: filtro no permitido, filtro vacío, cursor o límite inválidos, o una cabecera de navegador'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    e401:
      description: 'La llave no es válida, está revocada, venció, o la empresa dejó de ser PRO'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    e403:
      description: La llave no tiene el permiso que pide esta operación
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    e404:
      description: 'La ruta no existe, o tu empresa no tiene la API habilitada'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    e405:
      description: Método equivocado para esta ruta. La respuesta trae la cabecera Allow
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    e409:
      description: 'Misma Idempotency-Key con datos distintos'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    e413:
      description: El cuerpo pasa del máximo
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    e422:
      description: Un dato no es válido para tu empresa
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    e429:
      description: 'Pasaste un tope. La respuesta trae Retry-After'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    e500:
      description: Falló de nuestro lado. Reintenta y reporta el valor de peticion
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    e503:
      description: 'Tu empresa tiene demasiadas peticiones en curso, o la búsqueda no está disponible. Trae Retry-After'
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
  schemas:
    Error:
      type: object
      required: [ok, error]
      properties:
        ok: { type: boolean, example: false }
        error:
          type: object
          properties:
            codigo: { type: string }
            mensaje: { type: string }
            campo: { type: string, description: 'El parámetro que causó el error, cuando aplica' }
            peticion: { type: string, description: Pégalo en un correo a soporte; con eso lo encontramos en el log }
