{
  "openapi": "3.1.0",
  "info": {
    "title": "API de Cairos",
    "version": "1.0.0",
    "summary": "Contactos, productos y gastos de tu empresa, desde tu propio programa.",
    "description": "## Autenticación\n\nCada petición lleva la llave en la cabecera:\n\n```\nAuthorization: Bearer cai_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n```\n\nLa llave se genera dentro de Cairos, en la pantalla **Desarrolladores** (`/desarrolladores`), y **se enseña una sola vez**: no la guardamos, sólo su huella. Si la pierdes, genera otra.\n\nHay dos modos. `cai_live_` trabaja de verdad; `cai_test_` usa los mismos datos pero no registra en VeriFactu ni manda correos, que es lo que hace falta para desarrollar sin ensuciar una serie de facturación.\n\n## Permisos\n\nCada llave lleva una lista de permisos y **el de escritura no incluye el de lectura**. Es a propósito: una llave que sólo da de alta clientes no debería poder descargarse la cartera entera si alguien la roba. Si necesitas las dos cosas, pide las dos.\n\n## Paginación\n\nPor cursor, no por número de página. Pide una lista, y si el campo `siguiente` no es `null`, llama a esa URL. Repite hasta que sea `null`. Así no se pierden ni se repiten filas aunque alguien esté creando o borrando mientras recorres. Por defecto 50 elementos, máximo 200.\n\n## Límite de peticiones\n\n120 por minuto y por llave, en ventana deslizante. Mira `RateLimit-Remaining` para frenarte antes de que te frenemos nosotros; si llegas al 429, espera los segundos de `Retry-After`.\n\n## Reintentos sin duplicar\n\nManda una cabecera `Idempotency-Key` distinta en cada operación de escritura. Si la red se corta y reintentas, te devolvemos la respuesta de la primera vez en lugar de crear dos veces lo mismo.\n\n## Errores\n\nSiempre la misma forma:\n\n```json\n{ \"error\": { \"codigo\": \"no_encontrado\", \"mensaje\": \"…\", \"detalles\": null } }\n```\n\nEl `codigo` es estable y es lo que hay que mirar en el código; el `mensaje` está escrito para leerlo."
  },
  "servers": [
    {
      "url": "https://erp.cairos.es:3020/api/v1",
      "description": "Cairos"
    }
  ],
  "security": [
    {
      "llaveApi": []
    }
  ],
  "tags": [
    {
      "name": "General",
      "description": "Comprobar que la llave funciona."
    },
    {
      "name": "Contactos",
      "description": "Clientes y proveedores."
    },
    {
      "name": "Productos",
      "description": "Productos y servicios."
    },
    {
      "name": "Gastos",
      "description": "Facturas recibidas y otros gastos."
    },
    {
      "name": "Facturas",
      "description": "Emitir y consultar facturas de venta."
    },
    {
      "name": "Cobros",
      "description": "Lo que se ha cobrado de cada factura."
    },
    {
      "name": "Webhooks",
      "description": "Que Cairos te avise en vez de preguntar tú."
    }
  ],
  "components": {
    "securitySchemes": {
      "llaveApi": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "cai_live_…",
        "description": "Llave de la API de Cairos. Se genera dentro de Cairos, en la pantalla «Desarrolladores» (/desarrolladores)."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "title": "Error",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "codigo": {
                "type": "string",
                "enum": [
                  "no_autenticado",
                  "sin_permiso",
                  "no_encontrado",
                  "datos_invalidos",
                  "conflicto",
                  "demasiadas_peticiones",
                  "error_interno"
                ],
                "description": "Estable: es lo que debe mirar tu código."
              },
              "mensaje": {
                "type": "string",
                "description": "Explicación en castellano, para enseñarla o registrarla."
              },
              "detalles": {
                "description": "Depende del código. En «datos_invalidos» trae «campos», con el campo y el motivo de cada fallo."
              }
            },
            "required": [
              "codigo",
              "mensaje",
              "detalles"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "Indice": {
        "type": "object",
        "title": "Indice",
        "properties": {
          "api": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "hora": {
            "type": "string",
            "format": "date-time",
            "description": "Hora del servidor, para comprobar desfases de reloj."
          },
          "modo": {
            "type": "string",
            "enum": [
              "live",
              "test"
            ]
          },
          "empresa_id": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Lo que puede hacer ESTA llave."
          },
          "limite_peticiones": {
            "type": "object",
            "properties": {
              "peticiones": {
                "type": "integer"
              },
              "ventana_segundos": {
                "type": "integer"
              }
            },
            "required": [
              "peticiones",
              "ventana_segundos"
            ]
          },
          "openapi": {
            "type": "string",
            "description": "Dónde está esta misma documentación en formato máquina."
          }
        },
        "required": [
          "api",
          "version",
          "hora",
          "modo",
          "scopes"
        ]
      },
      "Contacto": {
        "type": "object",
        "title": "Contacto",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identificador del contacto"
          },
          "nombre": {
            "type": "string",
            "description": "Nombre comercial o razón social"
          },
          "nif": {
            "type": [
              "string",
              "null"
            ],
            "description": "NIF, CIF o NIE"
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Correo electrónico"
          },
          "telefono": {
            "type": [
              "string",
              "null"
            ],
            "description": "Teléfono fijo"
          },
          "movil": {
            "type": [
              "string",
              "null"
            ],
            "description": "Teléfono móvil"
          },
          "tipo": {
            "type": "string",
            "description": "customer o supplier"
          },
          "direccion": {
            "type": [
              "string",
              "null"
            ],
            "description": "Dirección"
          },
          "ciudad": {
            "type": [
              "string",
              "null"
            ],
            "description": "Población"
          },
          "cp": {
            "type": [
              "string",
              "null"
            ],
            "description": "Código postal"
          },
          "provincia": {
            "type": [
              "string",
              "null"
            ],
            "description": "Provincia"
          },
          "pais": {
            "type": [
              "string",
              "null"
            ],
            "description": "País"
          },
          "nif_iva": {
            "type": [
              "string",
              "null"
            ],
            "description": "NIF-IVA intracomunitario"
          },
          "web": {
            "type": [
              "string",
              "null"
            ],
            "description": "Página web"
          },
          "persona_contacto": {
            "type": [
              "string",
              "null"
            ],
            "description": "Persona de contacto"
          },
          "iban": {
            "type": [
              "string",
              "null"
            ],
            "description": "IBAN para domiciliaciones"
          },
          "dias_pago": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Días de vencimiento por defecto"
          },
          "forma_pago": {
            "type": [
              "string",
              "null"
            ],
            "description": "Forma de pago preferida"
          },
          "irpf": {
            "type": "integer",
            "description": "% de retención por defecto"
          },
          "descuento": {
            "type": "number",
            "description": "% de descuento por defecto"
          },
          "moneda": {
            "type": [
              "string",
              "null"
            ],
            "description": "Divisa (ISO 4217)"
          },
          "idioma": {
            "type": [
              "string",
              "null"
            ],
            "description": "Idioma de los documentos"
          },
          "referencia": {
            "type": [
              "string",
              "null"
            ],
            "description": "Referencia interna"
          },
          "etiquetas": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Etiquetas libres"
          },
          "notas": {
            "type": [
              "string",
              "null"
            ],
            "description": "Notas del contacto"
          },
          "creado": {
            "type": "string",
            "format": "date-time",
            "description": "Fecha de alta"
          }
        },
        "required": [
          "id",
          "nombre",
          "nif",
          "email",
          "telefono",
          "movil",
          "tipo",
          "direccion",
          "ciudad",
          "cp",
          "provincia",
          "pais",
          "nif_iva",
          "web",
          "persona_contacto",
          "iban",
          "dias_pago",
          "forma_pago",
          "irpf",
          "descuento",
          "moneda",
          "idioma",
          "referencia",
          "etiquetas",
          "notas",
          "creado"
        ]
      },
      "ContactoNuevo": {
        "type": "object",
        "title": "ContactoNuevo",
        "properties": {
          "nombre": {
            "type": "string",
            "maxLength": 200,
            "minLength": 1,
            "description": "Nombre comercial o razón social",
            "examples": [
              "Panadería Rosa SL"
            ]
          },
          "nif": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 20,
            "description": "NIF, CIF o NIE"
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "format": "email",
            "maxLength": 200
          },
          "telefono": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 40
          },
          "movil": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 40
          },
          "tipo": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "customer",
              "supplier"
            ],
            "description": "Cliente o proveedor. Por defecto, cliente"
          },
          "direccion": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 300
          },
          "ciudad": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          },
          "cp": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 12
          },
          "provincia": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          },
          "pais": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80
          },
          "nif_iva": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 20,
            "description": "NIF-IVA intracomunitario (ESB12345678)"
          },
          "web": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "persona_contacto": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "iban": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 40
          },
          "dias_pago": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 365,
            "description": "Días de vencimiento por defecto"
          },
          "forma_pago": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80
          },
          "irpf": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "% de retención por defecto"
          },
          "descuento": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "% de descuento por defecto"
          },
          "moneda": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 3,
            "minLength": 3,
            "examples": [
              "EUR"
            ]
          },
          "idioma": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10
          },
          "referencia": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60,
            "description": "Referencia interna del cliente"
          },
          "etiquetas": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "maxItems": 30
          },
          "notas": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 5000
          }
        },
        "required": [
          "nombre"
        ],
        "additionalProperties": false
      },
      "ContactoParche": {
        "type": "object",
        "title": "ContactoNuevoParche",
        "properties": {
          "nombre": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "minLength": 1,
            "description": "Nombre comercial o razón social",
            "examples": [
              "Panadería Rosa SL"
            ]
          },
          "nif": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 20,
            "description": "NIF, CIF o NIE"
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "format": "email",
            "maxLength": 200
          },
          "telefono": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 40
          },
          "movil": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 40
          },
          "tipo": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "customer",
              "supplier"
            ],
            "description": "Cliente o proveedor. Por defecto, cliente"
          },
          "direccion": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 300
          },
          "ciudad": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          },
          "cp": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 12
          },
          "provincia": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          },
          "pais": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80
          },
          "nif_iva": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 20,
            "description": "NIF-IVA intracomunitario (ESB12345678)"
          },
          "web": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "persona_contacto": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "iban": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 40
          },
          "dias_pago": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 365,
            "description": "Días de vencimiento por defecto"
          },
          "forma_pago": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 80
          },
          "irpf": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "% de retención por defecto"
          },
          "descuento": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "% de descuento por defecto"
          },
          "moneda": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 3,
            "minLength": 3,
            "examples": [
              "EUR"
            ]
          },
          "idioma": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 10
          },
          "referencia": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60,
            "description": "Referencia interna del cliente"
          },
          "etiquetas": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "maxItems": 30
          },
          "notas": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 5000
          }
        },
        "additionalProperties": false
      },
      "ListaContacto": {
        "type": "object",
        "title": "ListaContacto",
        "properties": {
          "datos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Contacto"
            }
          },
          "total": {
            "type": "integer",
            "description": "Cuántos hay en total con esos filtros, no en esta página."
          },
          "limite": {
            "type": "integer",
            "description": "Cuántos se han devuelto como máximo en esta página."
          },
          "siguiente": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL completa de la página siguiente, con los filtros ya puestos. «null» cuando no queda nada."
          }
        },
        "required": [
          "datos",
          "total",
          "limite",
          "siguiente"
        ]
      },
      "Producto": {
        "type": "object",
        "title": "Producto",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identificador del producto"
          },
          "nombre": {
            "type": "string",
            "description": "Nombre del producto o servicio"
          },
          "sku": {
            "type": [
              "string",
              "null"
            ],
            "description": "Referencia interna"
          },
          "codigo_barras": {
            "type": [
              "string",
              "null"
            ],
            "description": "Código de barras"
          },
          "descripcion": {
            "type": [
              "string",
              "null"
            ],
            "description": "Descripción"
          },
          "categoria": {
            "type": [
              "string",
              "null"
            ],
            "description": "Categoría"
          },
          "precio": {
            "type": "number",
            "description": "Precio de venta sin impuestos"
          },
          "tipo_iva": {
            "type": "number",
            "description": "% de IVA o IGIC"
          },
          "stock": {
            "type": "integer",
            "description": "Unidades en existencias"
          },
          "unidad": {
            "type": "string",
            "description": "Unidad de medida"
          },
          "clase": {
            "type": "string",
            "description": "goods (bien) o service (servicio)"
          },
          "stock_minimo": {
            "type": "integer",
            "description": "Existencias mínimas"
          },
          "etiquetas": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Etiquetas libres"
          },
          "creado": {
            "type": "string",
            "format": "date-time",
            "description": "Fecha de alta"
          }
        },
        "required": [
          "id",
          "nombre",
          "sku",
          "codigo_barras",
          "descripcion",
          "categoria",
          "precio",
          "tipo_iva",
          "stock",
          "unidad",
          "clase",
          "stock_minimo",
          "etiquetas",
          "creado"
        ]
      },
      "ProductoNuevo": {
        "type": "object",
        "title": "ProductoNuevo",
        "properties": {
          "nombre": {
            "type": "string",
            "maxLength": 200,
            "minLength": 1,
            "description": "Nombre del producto o servicio",
            "examples": [
              "Barra de pan"
            ]
          },
          "sku": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60,
            "description": "Referencia interna"
          },
          "codigo_barras": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 20,
            "description": "EAN-13, EAN-8 o UPC-A, sólo dígitos"
          },
          "descripcion": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 2000
          },
          "categoria": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          },
          "precio": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "description": "Precio de venta sin impuestos"
          },
          "tipo_iva": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "% de IVA o IGIC",
            "examples": [
              21
            ]
          },
          "stock": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unidades en existencias"
          },
          "unidad": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 20,
            "examples": [
              "ud"
            ]
          },
          "clase": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "goods",
              "service"
            ],
            "description": "Bien físico o servicio"
          },
          "stock_minimo": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Aviso por debajo de esta cantidad"
          },
          "etiquetas": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "maxItems": 30
          }
        },
        "required": [
          "nombre"
        ],
        "additionalProperties": false
      },
      "ProductoParche": {
        "type": "object",
        "title": "ProductoNuevoParche",
        "properties": {
          "nombre": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200,
            "minLength": 1,
            "description": "Nombre del producto o servicio",
            "examples": [
              "Barra de pan"
            ]
          },
          "sku": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60,
            "description": "Referencia interna"
          },
          "codigo_barras": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 20,
            "description": "EAN-13, EAN-8 o UPC-A, sólo dígitos"
          },
          "descripcion": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 2000
          },
          "categoria": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          },
          "precio": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "description": "Precio de venta sin impuestos"
          },
          "tipo_iva": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "% de IVA o IGIC",
            "examples": [
              21
            ]
          },
          "stock": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Unidades en existencias"
          },
          "unidad": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 20,
            "examples": [
              "ud"
            ]
          },
          "clase": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "goods",
              "service"
            ],
            "description": "Bien físico o servicio"
          },
          "stock_minimo": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Aviso por debajo de esta cantidad"
          },
          "etiquetas": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "maxItems": 30
          }
        },
        "additionalProperties": false
      },
      "ListaProducto": {
        "type": "object",
        "title": "ListaProducto",
        "properties": {
          "datos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Producto"
            }
          },
          "total": {
            "type": "integer",
            "description": "Cuántos hay en total con esos filtros, no en esta página."
          },
          "limite": {
            "type": "integer",
            "description": "Cuántos se han devuelto como máximo en esta página."
          },
          "siguiente": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL completa de la página siguiente, con los filtros ya puestos. «null» cuando no queda nada."
          }
        },
        "required": [
          "datos",
          "total",
          "limite",
          "siguiente"
        ]
      },
      "Gasto": {
        "type": "object",
        "title": "Gasto",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identificador del gasto"
          },
          "proveedor_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Id del contacto proveedor"
          },
          "proveedor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nombre del proveedor"
          },
          "referencia": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nº de factura del proveedor"
          },
          "categoria": {
            "type": [
              "string",
              "null"
            ],
            "description": "Categoría de gasto"
          },
          "clase": {
            "type": [
              "string",
              "null"
            ],
            "description": "goods o service"
          },
          "fecha": {
            "type": "string",
            "format": "date-time",
            "description": "Fecha de la factura"
          },
          "base": {
            "type": "number",
            "description": "Base imponible"
          },
          "tipo_iva": {
            "type": "number",
            "description": "% de IVA o IGIC"
          },
          "cuota_iva": {
            "type": "number",
            "description": "Cuota de IVA o IGIC (calculada)"
          },
          "irpf": {
            "type": "integer",
            "description": "% de retención practicada"
          },
          "total": {
            "type": "number",
            "description": "Base más impuestos (calculado)"
          },
          "pagado": {
            "type": "boolean",
            "description": "¿Está pagado?"
          },
          "notas": {
            "type": [
              "string",
              "null"
            ],
            "description": "Notas"
          },
          "estado_aprobacion": {
            "type": "string",
            "description": "none, pending, approved o rejected"
          },
          "creado": {
            "type": "string",
            "format": "date-time",
            "description": "Fecha de alta en Cairos"
          }
        },
        "required": [
          "id",
          "proveedor_id",
          "proveedor",
          "referencia",
          "categoria",
          "clase",
          "fecha",
          "base",
          "tipo_iva",
          "cuota_iva",
          "irpf",
          "total",
          "pagado",
          "notas",
          "estado_aprobacion",
          "creado"
        ]
      },
      "GastoNuevo": {
        "type": "object",
        "title": "GastoNuevo",
        "properties": {
          "base": {
            "type": "number",
            "description": "Base imponible, sin impuestos",
            "examples": [
              100
            ]
          },
          "proveedor_id": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 40,
            "description": "Id de un contacto de tipo supplier"
          },
          "referencia": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 60,
            "description": "Nº de factura del proveedor"
          },
          "categoria": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 120
          },
          "clase": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "goods",
              "service"
            ],
            "description": "Bien o servicio (clave del modelo 349)"
          },
          "fecha": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Fecha de la factura. Por defecto, hoy"
          },
          "tipo_iva": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "% de IVA o IGIC soportado",
            "examples": [
              21
            ]
          },
          "irpf": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100,
            "description": "% de retención practicada al proveedor"
          },
          "pagado": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "notas": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 5000
          }
        },
        "required": [
          "base"
        ],
        "additionalProperties": false
      },
      "ListaGasto": {
        "type": "object",
        "title": "ListaGasto",
        "properties": {
          "datos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Gasto"
            }
          },
          "total": {
            "type": "integer",
            "description": "Cuántos hay en total con esos filtros, no en esta página."
          },
          "limite": {
            "type": "integer",
            "description": "Cuántos se han devuelto como máximo en esta página."
          },
          "siguiente": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL completa de la página siguiente, con los filtros ya puestos. «null» cuando no queda nada."
          }
        },
        "required": [
          "datos",
          "total",
          "limite",
          "siguiente"
        ]
      },
      "Organizacion": {
        "type": "object",
        "title": "Organizacion",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identificador de la empresa"
          },
          "nombre": {
            "type": "string",
            "description": "Nombre comercial"
          },
          "razon_social": {
            "type": [
              "string",
              "null"
            ],
            "description": "Razón social"
          },
          "nif": {
            "type": [
              "string",
              "null"
            ],
            "description": "NIF o CIF"
          },
          "direccion": {
            "type": [
              "string",
              "null"
            ],
            "description": "Dirección"
          },
          "ciudad": {
            "type": [
              "string",
              "null"
            ],
            "description": "Población"
          },
          "cp": {
            "type": [
              "string",
              "null"
            ],
            "description": "Código postal"
          },
          "provincia": {
            "type": [
              "string",
              "null"
            ],
            "description": "Provincia"
          },
          "pais": {
            "type": [
              "string",
              "null"
            ],
            "description": "País"
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Correo de contacto"
          },
          "telefono": {
            "type": [
              "string",
              "null"
            ],
            "description": "Teléfono"
          },
          "web": {
            "type": [
              "string",
              "null"
            ],
            "description": "Página web"
          },
          "forma_juridica": {
            "type": [
              "string",
              "null"
            ],
            "description": "SL, SA, autónomo, cooperativa…"
          },
          "sector": {
            "type": [
              "string",
              "null"
            ],
            "description": "Sector de actividad"
          },
          "moneda": {
            "type": "string",
            "description": "Divisa por defecto"
          },
          "region_fiscal": {
            "type": "string",
            "description": "peninsula, canarias, ceuta o melilla"
          },
          "tipo_iva_defecto": {
            "type": "integer",
            "description": "% de impuesto por defecto"
          },
          "regimen_igic": {
            "type": "string",
            "description": "general, repep o minorista"
          },
          "recargo_equivalencia": {
            "type": "boolean",
            "description": "¿Está en recargo de equivalencia?"
          },
          "sin_animo_lucro": {
            "type": "boolean",
            "description": "¿Es una entidad sin ánimo de lucro?"
          },
          "plan_contable": {
            "type": "string",
            "description": "pgc o esfl"
          },
          "creada": {
            "type": "string",
            "format": "date-time",
            "description": "Fecha de alta en Cairos"
          }
        },
        "required": [
          "id",
          "nombre",
          "razon_social",
          "nif",
          "direccion",
          "ciudad",
          "cp",
          "provincia",
          "pais",
          "email",
          "telefono",
          "web",
          "forma_juridica",
          "sector",
          "moneda",
          "region_fiscal",
          "tipo_iva_defecto",
          "regimen_igic",
          "recargo_equivalencia",
          "sin_animo_lucro",
          "plan_contable",
          "creada"
        ]
      },
      "Factura": {
        "type": "object",
        "title": "Factura",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identificador de la factura"
          },
          "serie": {
            "type": "string",
            "description": "Serie de numeración"
          },
          "numero": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Número dentro de la serie. NULO mientras es borrador: un borrador no ocupa número, se le reserva al emitirlo"
          },
          "referencia": {
            "type": [
              "string",
              "null"
            ],
            "description": "Serie y número juntos («F-0008»). Nulo mientras es borrador"
          },
          "tipo_documento": {
            "type": "string",
            "description": "invoice, quote, delivery o proforma"
          },
          "tipo": {
            "type": "string",
            "description": "invoice (normal) o rectificative (rectificativa)"
          },
          "estado": {
            "type": "string",
            "enum": [
              "draft",
              "sent",
              "paid",
              "overdue"
            ],
            "description": "draft, sent, paid u overdue"
          },
          "fecha_emision": {
            "type": "string",
            "format": "date",
            "description": "Fecha de emisión"
          },
          "fecha_vencimiento": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Primer vencimiento. Con plazos 30/60/90, el de 30"
          },
          "plazos": {
            "type": [
              "string",
              "null"
            ],
            "description": "Patrón de vencimientos pactado, tal cual se escribió: «30/60/90»"
          },
          "forma_pago": {
            "type": [
              "string",
              "null"
            ],
            "description": "transfer, cash, card, direct_debit u other"
          },
          "fecha_cobro": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Cuándo quedó cobrada del todo"
          },
          "notas": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nota que SÍ sale impresa en el documento"
          },
          "etiquetas": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Etiquetas libres"
          },
          "divisa": {
            "type": "string",
            "description": "Divisa del documento (ISO 4217)"
          },
          "tipo_cambio": {
            "type": "number",
            "description": "Tipo de cambio congelado el día del devengo (art. 79.Once de la Ley 37/1992)"
          },
          "base_imponible": {
            "type": "number",
            "description": "Suma de las líneas, ya descontados los descuentos"
          },
          "cuota_iva": {
            "type": "number",
            "description": "Cuota de IVA o IGIC"
          },
          "recargo_equivalencia": {
            "type": "boolean",
            "description": "¿Lleva recargo de equivalencia (o el minorista, en Canarias)?"
          },
          "cuota_recargo": {
            "type": "number",
            "description": "Cuota del recargo"
          },
          "irpf": {
            "type": "integer",
            "description": "% de retención aplicada"
          },
          "cuota_irpf": {
            "type": "number",
            "description": "Importe retenido, que se RESTA del total"
          },
          "suplidos": {
            "type": "number",
            "description": "Suplidos: se suman al total y no llevan impuesto"
          },
          "total": {
            "type": "number",
            "description": "Lo que hay que pagar: base + impuestos + recargo − IRPF + suplidos"
          },
          "cobrado": {
            "type": "number",
            "description": "Suma de los cobros registrados"
          },
          "pendiente": {
            "type": "number",
            "description": "Total menos lo cobrado, nunca negativo"
          },
          "creado": {
            "type": "string",
            "format": "date-time",
            "description": "Cuándo se dio de alta en Cairos"
          },
          "verifactu": {
            "type": "object",
            "properties": {
              "registrada": {
                "type": "boolean",
                "description": "¿Tiene ya su huella en la cadena?"
              },
              "huella": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Huella SHA-256 del registro"
              },
              "registrada_en": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "Cuándo se encadenó"
              },
              "aeat": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Qué contestó la AEAT, si la empresa tiene certificado configurado"
              }
            },
            "required": [
              "registrada",
              "huella",
              "registrada_en",
              "aeat"
            ],
            "description": "Estado del registro en VeriFactu. Una llave de pruebas NO registra: sale «registrada: false»"
          },
          "contacto": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string",
                "description": "Su id en /api/v1/contactos"
              },
              "nombre": {
                "type": "string",
                "description": "Nombre o razón social"
              },
              "nif": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "NIF"
              },
              "email": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Correo"
              }
            },
            "required": [
              "id",
              "nombre",
              "nif",
              "email"
            ],
            "description": "El cliente. Nulo en una factura sin cliente asignado"
          },
          "lineas": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Identificador de la línea"
                },
                "producto_id": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Producto del catálogo, si la línea salió de uno"
                },
                "descripcion": {
                  "type": "string",
                  "description": "Lo que se factura, tal y como sale impreso"
                },
                "cantidad": {
                  "type": "number",
                  "description": "Unidades"
                },
                "precio": {
                  "type": "number",
                  "description": "Precio unitario sin impuestos"
                },
                "descuento": {
                  "type": "number",
                  "description": "% de descuento sobre cantidad × precio"
                },
                "tipo_iva": {
                  "type": "number",
                  "description": "% de IVA o IGIC de la línea"
                },
                "clase": {
                  "type": "string",
                  "description": "goods (bien) o service (servicio). Decide la clave del modelo 349 y se congela al emitir"
                },
                "unidad": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Unidad de medida: ud, h, kg…"
                }
              },
              "required": [
                "id",
                "producto_id",
                "descripcion",
                "cantidad",
                "precio",
                "descuento",
                "tipo_iva",
                "clase",
                "unidad"
              ]
            },
            "description": "Las líneas del documento. OJO: en los LISTADOS esta clave NO viene, y no viene a propósito — un «[]» diría que la factura no tiene líneas y quien integra se lo creería. Pide la factura suelta para verlas"
          }
        },
        "required": [
          "id",
          "serie",
          "numero",
          "referencia",
          "tipo_documento",
          "tipo",
          "estado",
          "fecha_emision",
          "fecha_vencimiento",
          "plazos",
          "forma_pago",
          "fecha_cobro",
          "notas",
          "etiquetas",
          "divisa",
          "tipo_cambio",
          "base_imponible",
          "cuota_iva",
          "recargo_equivalencia",
          "cuota_recargo",
          "irpf",
          "cuota_irpf",
          "suplidos",
          "total",
          "cobrado",
          "pendiente",
          "creado",
          "verifactu",
          "contacto"
        ]
      },
      "FacturaNueva": {
        "type": "object",
        "title": "FacturaNueva",
        "properties": {
          "serie": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 16
          },
          "numero": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1
          },
          "contacto_id": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 64
          },
          "fecha_emision": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "fecha_vencimiento": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "plazos": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 64
          },
          "forma_pago": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "transfer",
              "cash",
              "card",
              "direct_debit",
              "other"
            ]
          },
          "notas": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 4000
          },
          "notas_internas": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 4000
          },
          "etiquetas": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            },
            "maxItems": 12
          },
          "divisa": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 3
          },
          "tipo_cambio": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0
          },
          "irpf": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 100
          },
          "recargo_equivalencia": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "suplidos": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0
          },
          "almacen_id": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 64
          },
          "emitir": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "lineas": {
            "type": "array",
            "items": {
              "type": "object",
              "title": "LineaFactura",
              "properties": {
                "producto_id": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "maxLength": 64
                },
                "descripcion": {
                  "type": "string",
                  "maxLength": 500
                },
                "cantidad": {
                  "type": "number"
                },
                "precio": {
                  "type": "number"
                },
                "tipo_iva": {
                  "type": [
                    "number",
                    "null"
                  ],
                  "minimum": 0,
                  "maximum": 100
                },
                "descuento": {
                  "type": [
                    "number",
                    "null"
                  ],
                  "minimum": 0,
                  "maximum": 100
                },
                "clase": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "enum": [
                    "goods",
                    "service"
                  ]
                },
                "unidad": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "maxLength": 20
                }
              },
              "required": [
                "descripcion",
                "cantidad",
                "precio"
              ],
              "additionalProperties": false
            },
            "minItems": 1,
            "maxItems": 300
          }
        },
        "required": [
          "lineas"
        ],
        "additionalProperties": false
      },
      "ListaFactura": {
        "type": "object",
        "title": "ListaFactura",
        "properties": {
          "datos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Factura"
            }
          },
          "total": {
            "type": "integer",
            "description": "Cuántos hay en total con esos filtros, no en esta página."
          },
          "limite": {
            "type": "integer",
            "description": "Cuántos se han devuelto como máximo en esta página."
          },
          "siguiente": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL completa de la página siguiente, con los filtros ya puestos. «null» cuando no queda nada."
          }
        },
        "required": [
          "datos",
          "total",
          "limite",
          "siguiente"
        ]
      },
      "Cobro": {
        "type": "object",
        "title": "Cobro",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identificador del cobro"
          },
          "factura_id": {
            "type": "string",
            "description": "Factura a la que se imputa"
          },
          "importe": {
            "type": "number",
            "description": "Importe cobrado"
          },
          "fecha": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Fecha del cobro"
          },
          "metodo": {
            "type": [
              "string",
              "null"
            ],
            "description": "transfer, cash, card, direct_debit u other"
          },
          "notas": {
            "type": [
              "string",
              "null"
            ],
            "description": "Nota del cobro"
          },
          "creado": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Cuándo se registró en Cairos"
          }
        },
        "required": [
          "id",
          "factura_id",
          "importe",
          "fecha",
          "metodo",
          "notas",
          "creado"
        ]
      },
      "CobroNuevo": {
        "type": "object",
        "title": "CobroNuevo",
        "properties": {
          "factura_id": {
            "type": "string",
            "maxLength": 64
          },
          "importe": {
            "type": "number",
            "minimum": 0
          },
          "fecha": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "metodo": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "transfer",
              "cash",
              "card",
              "direct_debit",
              "other"
            ]
          },
          "notas": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 1000
          }
        },
        "required": [
          "factura_id",
          "importe"
        ],
        "additionalProperties": false
      },
      "ListaCobro": {
        "type": "object",
        "title": "ListaCobro",
        "properties": {
          "datos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Cobro"
            }
          },
          "total": {
            "type": "integer",
            "description": "Cuántos hay en total con esos filtros, no en esta página."
          },
          "limite": {
            "type": "integer",
            "description": "Cuántos se han devuelto como máximo en esta página."
          },
          "siguiente": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL completa de la página siguiente, con los filtros ya puestos. «null» cuando no queda nada."
          }
        },
        "required": [
          "datos",
          "total",
          "limite",
          "siguiente"
        ]
      },
      "Webhook": {
        "type": "object",
        "title": "Webhook",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identificador del webhook"
          },
          "url": {
            "type": "string",
            "description": "A dónde se manda el aviso"
          },
          "sucesos": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "A qué está suscrito. «*» son todos"
          },
          "activo": {
            "type": "boolean",
            "description": "¿Se le manda algo?"
          },
          "descripcion": {
            "type": [
              "string",
              "null"
            ],
            "description": "Para qué es, para reconocerlo en la lista"
          },
          "ultimo_ok": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Última entrega correcta"
          },
          "ultimo_fallo": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Último fallo"
          },
          "fallos_seguidos": {
            "type": "integer",
            "description": "Fallos encadenados. Al llegar al tope, el webhook se desactiva solo"
          },
          "creado": {
            "type": "string",
            "format": "date-time",
            "description": "Cuándo se dio de alta"
          }
        },
        "required": [
          "id",
          "url",
          "sucesos",
          "activo",
          "descripcion",
          "ultimo_ok",
          "ultimo_fallo",
          "fallos_seguidos",
          "creado"
        ]
      },
      "WebhookNuevo": {
        "type": "object",
        "title": "WebhookNuevo",
        "properties": {
          "url": {
            "type": "string",
            "maxLength": 500
          },
          "sucesos": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 20
          },
          "descripcion": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 200
          },
          "activo": {
            "type": [
              "boolean",
              "null"
            ]
          }
        },
        "required": [
          "url",
          "sucesos"
        ],
        "additionalProperties": false
      },
      "WebhookCreado": {
        "type": "object",
        "title": "WebhookCreado",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identificador del webhook"
          },
          "url": {
            "type": "string",
            "description": "A dónde se manda el aviso"
          },
          "sucesos": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "A qué está suscrito. «*» son todos"
          },
          "activo": {
            "type": "boolean",
            "description": "¿Se le manda algo?"
          },
          "descripcion": {
            "type": [
              "string",
              "null"
            ],
            "description": "Para qué es, para reconocerlo en la lista"
          },
          "ultimo_ok": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Última entrega correcta"
          },
          "ultimo_fallo": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Último fallo"
          },
          "fallos_seguidos": {
            "type": "integer",
            "description": "Fallos encadenados. Al llegar al tope, el webhook se desactiva solo"
          },
          "creado": {
            "type": "string",
            "format": "date-time",
            "description": "Cuándo se dio de alta"
          },
          "secreto": {
            "type": "string",
            "description": "El secreto con el que se firma cada aviso. SE ENSEÑA UNA SOLA VEZ: guárdalo ahora, no se puede volver a consultar"
          },
          "firma": {
            "type": "object",
            "properties": {
              "cabecera": {
                "type": "string",
                "description": "En qué cabecera viaja la firma"
              },
              "formato": {
                "type": "string",
                "description": "Cómo está compuesta"
              },
              "se_firma": {
                "type": "string",
                "description": "Qué texto exacto se firma"
              },
              "aviso": {
                "type": "string",
                "description": "Por qué hay que mirar además la marca de tiempo"
              }
            },
            "required": [
              "cabecera",
              "formato",
              "se_firma",
              "aviso"
            ],
            "description": "Cómo comprobar que un aviso viene de Cairos y no de cualquiera que sepa tu URL"
          },
          "sucesos_disponibles": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Todos los sucesos a los que se puede uno suscribir"
          }
        },
        "required": [
          "id",
          "url",
          "sucesos",
          "activo",
          "descripcion",
          "ultimo_ok",
          "ultimo_fallo",
          "fallos_seguidos",
          "creado",
          "secreto",
          "firma",
          "sucesos_disponibles"
        ]
      },
      "ListaWebhook": {
        "type": "object",
        "title": "ListaWebhook",
        "properties": {
          "datos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Webhook"
            }
          },
          "total": {
            "type": "integer",
            "description": "Cuántos hay en total con esos filtros, no en esta página."
          },
          "limite": {
            "type": "integer",
            "description": "Cuántos se han devuelto como máximo en esta página."
          },
          "siguiente": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL completa de la página siguiente, con los filtros ya puestos. «null» cuando no queda nada."
          }
        },
        "required": [
          "datos",
          "total",
          "limite",
          "siguiente"
        ]
      },
      "EnvioWebhook": {
        "type": "object",
        "title": "EnvioWebhook",
        "properties": {
          "id": {
            "type": "string",
            "description": "Identificador del envío"
          },
          "suceso": {
            "type": "string",
            "description": "Qué pasó"
          },
          "estado": {
            "type": "string",
            "description": "pending, sent o failed"
          },
          "intentos": {
            "type": "integer",
            "description": "Cuántas veces se ha intentado"
          },
          "http": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Código que contestó tu servidor"
          },
          "respuesta": {
            "type": [
              "string",
              "null"
            ],
            "description": "Lo que contestó, recortado"
          },
          "siguiente_intento": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Cuándo se reintentará"
          },
          "entregado": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Cuándo se entregó bien"
          },
          "creado": {
            "type": "string",
            "format": "date-time",
            "description": "Cuándo se encoló"
          },
          "cuerpo": {
            "type": "string",
            "description": "El JSON que se mandó, tal cual, para poder compararlo con lo que recibiste"
          }
        },
        "required": [
          "id",
          "suceso",
          "estado",
          "intentos",
          "http",
          "respuesta",
          "siguiente_intento",
          "entregado",
          "creado",
          "cuerpo"
        ]
      },
      "ListaEnvioWebhook": {
        "type": "object",
        "title": "ListaEnvioWebhook",
        "properties": {
          "datos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EnvioWebhook"
            }
          },
          "total": {
            "type": "integer",
            "description": "Cuántos hay en total con esos filtros, no en esta página."
          },
          "limite": {
            "type": "integer",
            "description": "Cuántos se han devuelto como máximo en esta página."
          },
          "siguiente": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL completa de la página siguiente, con los filtros ya puestos. «null» cuando no queda nada."
          }
        },
        "required": [
          "datos",
          "total",
          "limite",
          "siguiente"
        ]
      },
      "PruebaWebhook": {
        "type": "object",
        "title": "PruebaWebhook",
        "properties": {
          "entregado": {
            "type": "boolean",
            "description": "¿Contestó tu servidor con un 2xx?"
          },
          "http": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Código que contestó"
          },
          "respuesta": {
            "type": [
              "string",
              "null"
            ],
            "description": "Lo que contestó, recortado"
          },
          "recuerda": {
            "type": "string",
            "description": "Recordatorio de que hay que comprobar la firma"
          }
        },
        "required": [
          "entregado",
          "http",
          "respuesta",
          "recuerda"
        ]
      },
      "BorradoSimple": {
        "type": "object",
        "title": "BorradoSimple",
        "properties": {
          "id": {
            "type": "string"
          },
          "borrado": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "borrado"
        ]
      },
      "Borrado": {
        "type": "object",
        "title": "Borrado",
        "properties": {
          "id": {
            "type": "string"
          },
          "borrado": {
            "type": "boolean"
          },
          "papelera": {
            "type": "boolean",
            "description": "Se ha guardado una copia en la papelera de Cairos, recuperable durante 30 días."
          }
        },
        "required": [
          "id",
          "borrado",
          "papelera"
        ]
      }
    }
  },
  "paths": {
    "/": {
      "get": {
        "tags": [
          "General"
        ],
        "operationId": "indice",
        "summary": "Comprobar la llave",
        "description": "Lo primero que hay que llamar: dice si la llave vale, qué permisos tiene y qué hora es en el servidor. No necesita ningún permiso concreto, basta con una llave válida.",
        "responses": {
          "200": {
            "description": "La llave funciona.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Indice"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": [
          "General"
        ],
        "operationId": "openapi",
        "summary": "Esta misma documentación",
        "description": "Se sirve sin llave, porque describe el contrato y no contiene datos de ninguna empresa.",
        "security": [],
        "responses": {
          "200": {
            "description": "El documento OpenAPI 3.1.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/organizacion": {
      "get": {
        "tags": [
          "General"
        ],
        "operationId": "organizacion",
        "summary": "Datos de la empresa",
        "description": "Los datos fiscales y de contacto de la empresa a la que pertenece la llave.",
        "responses": {
          "200": {
            "description": "Los datos de la empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organizacion"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/contactos": {
      "get": {
        "tags": [
          "Contactos"
        ],
        "operationId": "listarContactos",
        "summary": "Listar clientes y proveedores",
        "description": "Permiso: `contacts:read`. «buscar» mira el nombre, el NIF, el correo, el teléfono y la referencia.",
        "security": [
          {
            "llaveApi": [
              "contacts:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "limite",
            "in": "query",
            "description": "Cuántos elementos devolver. Por defecto 50, máximo 200. Pedir más no da error: se recorta.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "desde",
            "in": "query",
            "description": "Cursor opaco. No lo construyas: cópialo del campo «siguiente» de la respuesta anterior, o sigue directamente esa URL.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "orden",
            "in": "query",
            "description": "«reciente» empieza por lo último creado (por defecto). «antiguo» empieza por lo primero, que es lo que quieres para recorrer el histórico entero. El cursor no se puede mezclar entre órdenes.",
            "schema": {
              "type": "string",
              "enum": [
                "reciente",
                "antiguo"
              ],
              "default": "reciente"
            }
          },
          {
            "name": "buscar",
            "in": "query",
            "description": "Texto libre. Busca sin distinguir mayúsculas en los campos principales del recurso.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "actualizado_desde",
            "in": "query",
            "description": "Sólo lo modificado a partir de esta fecha, contando el alta como una modificación. Es el filtro con el que se hace una sincronización incremental: guarda la hora de tu última pasada y pásala aquí en la siguiente. AVISO, y sólo la primera vez: las filas anteriores a que Cairos empezara a guardar la fecha de modificación se quedaron con la fecha de esa migración, así que tu primera sincronización las verá todas como cambiadas.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "actualizado_hasta",
            "in": "query",
            "description": "El otro extremo de la ventana. Se incluye la fecha indicada.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "tipo",
            "in": "query",
            "description": "Sólo clientes o sólo proveedores.",
            "schema": {
              "type": "string",
              "enum": [
                "customer",
                "supplier"
              ]
            }
          },
          {
            "name": "etiqueta",
            "in": "query",
            "description": "Sólo los que llevan esta etiqueta.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Una página de contactos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListaContacto"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Contactos"
        ],
        "operationId": "crearContacto",
        "summary": "Crear un cliente o proveedor",
        "description": "Permiso: `contacts:write`.",
        "security": [
          {
            "llaveApi": [
              "contacts:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Opcional pero muy recomendable. Una cadena única por operación (por ejemplo el id del pedido en tu tienda). Si repites la petición con la misma llave y el mismo cuerpo, se devuelve la respuesta guardada en vez de crear otra vez. La misma llave con otro cuerpo devuelve 409. Se recuerda 24 horas.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactoNuevo"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "El contacto creado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Contacto"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "La Idempotency-Key ya se usó con otro contenido, o hay otra petición con esa misma llave todavía en marcha.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/contactos/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Contactos"
        ],
        "operationId": "verContacto",
        "summary": "Ver un contacto",
        "description": "Permiso: `contacts:read`.",
        "security": [
          {
            "llaveApi": [
              "contacts:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "El contacto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Contacto"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No existe ese recurso en esta empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Contactos"
        ],
        "operationId": "modificarContacto",
        "summary": "Modificar un contacto",
        "description": "Permiso: `contacts:write`. Sólo cambia los campos que mandes; los que no vengan se quedan como estaban. Manda `null` para vaciar uno.",
        "security": [
          {
            "llaveApi": [
              "contacts:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Opcional pero muy recomendable. Una cadena única por operación (por ejemplo el id del pedido en tu tienda). Si repites la petición con la misma llave y el mismo cuerpo, se devuelve la respuesta guardada en vez de crear otra vez. La misma llave con otro cuerpo devuelve 409. Se recuerda 24 horas.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactoParche"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "El contacto ya modificado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Contacto"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No existe ese recurso en esta empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "La Idempotency-Key ya se usó con otro contenido, o hay otra petición con esa misma llave todavía en marcha.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Contactos"
        ],
        "operationId": "borrarContacto",
        "summary": "Borrar un contacto",
        "description": "Permiso: `contacts:write`. Va a la papelera de Cairos y se puede recuperar durante 30 días. Un contacto con facturas o gastos no se borra: devuelve 409.",
        "security": [
          {
            "llaveApi": [
              "contacts:write"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Borrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Borrado"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No existe ese recurso en esta empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "La Idempotency-Key ya se usó con otro contenido, o hay otra petición con esa misma llave todavía en marcha.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/productos": {
      "get": {
        "tags": [
          "Productos"
        ],
        "operationId": "listarProductos",
        "summary": "Listar productos y servicios",
        "description": "Permiso: `products:read`. «buscar» mira el nombre, el SKU, el código de barras y la descripción.",
        "security": [
          {
            "llaveApi": [
              "products:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "limite",
            "in": "query",
            "description": "Cuántos elementos devolver. Por defecto 50, máximo 200. Pedir más no da error: se recorta.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "desde",
            "in": "query",
            "description": "Cursor opaco. No lo construyas: cópialo del campo «siguiente» de la respuesta anterior, o sigue directamente esa URL.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "orden",
            "in": "query",
            "description": "«reciente» empieza por lo último creado (por defecto). «antiguo» empieza por lo primero, que es lo que quieres para recorrer el histórico entero. El cursor no se puede mezclar entre órdenes.",
            "schema": {
              "type": "string",
              "enum": [
                "reciente",
                "antiguo"
              ],
              "default": "reciente"
            }
          },
          {
            "name": "buscar",
            "in": "query",
            "description": "Texto libre. Busca sin distinguir mayúsculas en los campos principales del recurso.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "actualizado_desde",
            "in": "query",
            "description": "Sólo lo modificado a partir de esta fecha, contando el alta como una modificación. Es el filtro con el que se hace una sincronización incremental: guarda la hora de tu última pasada y pásala aquí en la siguiente. AVISO, y sólo la primera vez: las filas anteriores a que Cairos empezara a guardar la fecha de modificación se quedaron con la fecha de esa migración, así que tu primera sincronización las verá todas como cambiadas.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "actualizado_hasta",
            "in": "query",
            "description": "El otro extremo de la ventana. Se incluye la fecha indicada.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "categoria",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "clase",
            "in": "query",
            "description": "Bienes o servicios.",
            "schema": {
              "type": "string",
              "enum": [
                "goods",
                "service"
              ]
            }
          },
          {
            "name": "sku",
            "in": "query",
            "description": "Búsqueda exacta por referencia.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "codigo_barras",
            "in": "query",
            "description": "Búsqueda exacta por código de barras.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "bajo_minimo",
            "in": "query",
            "description": "Sólo los que están por debajo de sus existencias mínimas.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Una página de productos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListaProducto"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Productos"
        ],
        "operationId": "crearProducto",
        "summary": "Crear un producto o servicio",
        "description": "Permiso: `products:write`. El código de barras no se puede repetir dentro de la misma empresa.",
        "security": [
          {
            "llaveApi": [
              "products:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Opcional pero muy recomendable. Una cadena única por operación (por ejemplo el id del pedido en tu tienda). Si repites la petición con la misma llave y el mismo cuerpo, se devuelve la respuesta guardada en vez de crear otra vez. La misma llave con otro cuerpo devuelve 409. Se recuerda 24 horas.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProductoNuevo"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "El producto creado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Producto"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "La Idempotency-Key ya se usó con otro contenido, o hay otra petición con esa misma llave todavía en marcha.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/productos/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Productos"
        ],
        "operationId": "verProducto",
        "summary": "Ver un producto",
        "description": "Permiso: `products:read`.",
        "security": [
          {
            "llaveApi": [
              "products:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "El producto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Producto"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No existe ese recurso en esta empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Productos"
        ],
        "operationId": "modificarProducto",
        "summary": "Modificar un producto",
        "description": "Permiso: `products:write`. Sólo cambia los campos que mandes.",
        "security": [
          {
            "llaveApi": [
              "products:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Opcional pero muy recomendable. Una cadena única por operación (por ejemplo el id del pedido en tu tienda). Si repites la petición con la misma llave y el mismo cuerpo, se devuelve la respuesta guardada en vez de crear otra vez. La misma llave con otro cuerpo devuelve 409. Se recuerda 24 horas.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProductoParche"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "El producto ya modificado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Producto"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No existe ese recurso en esta empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "La Idempotency-Key ya se usó con otro contenido, o hay otra petición con esa misma llave todavía en marcha.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Productos"
        ],
        "operationId": "borrarProducto",
        "summary": "Borrar un producto",
        "description": "Permiso: `products:write`. Va a la papelera durante 30 días. Un producto que aparece en alguna factura no se borra: devuelve 409.",
        "security": [
          {
            "llaveApi": [
              "products:write"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Borrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Borrado"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No existe ese recurso en esta empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "La Idempotency-Key ya se usó con otro contenido, o hay otra petición con esa misma llave todavía en marcha.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/gastos": {
      "get": {
        "tags": [
          "Gastos"
        ],
        "operationId": "listarGastos",
        "summary": "Listar gastos",
        "description": "Permiso: `expenses:read`. «buscar» mira la referencia, la categoría y las notas.",
        "security": [
          {
            "llaveApi": [
              "expenses:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "limite",
            "in": "query",
            "description": "Cuántos elementos devolver. Por defecto 50, máximo 200. Pedir más no da error: se recorta.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "desde",
            "in": "query",
            "description": "Cursor opaco. No lo construyas: cópialo del campo «siguiente» de la respuesta anterior, o sigue directamente esa URL.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "orden",
            "in": "query",
            "description": "«reciente» empieza por lo último creado (por defecto). «antiguo» empieza por lo primero, que es lo que quieres para recorrer el histórico entero. El cursor no se puede mezclar entre órdenes.",
            "schema": {
              "type": "string",
              "enum": [
                "reciente",
                "antiguo"
              ],
              "default": "reciente"
            }
          },
          {
            "name": "buscar",
            "in": "query",
            "description": "Texto libre. Busca sin distinguir mayúsculas en los campos principales del recurso.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "actualizado_desde",
            "in": "query",
            "description": "Sólo lo modificado a partir de esta fecha, contando el alta como una modificación. Es el filtro con el que se hace una sincronización incremental: guarda la hora de tu última pasada y pásala aquí en la siguiente. AVISO, y sólo la primera vez: las filas anteriores a que Cairos empezara a guardar la fecha de modificación se quedaron con la fecha de esa migración, así que tu primera sincronización las verá todas como cambiadas.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "actualizado_hasta",
            "in": "query",
            "description": "El otro extremo de la ventana. Se incluye la fecha indicada.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "proveedor_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "categoria",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pagado",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "fecha_desde",
            "in": "query",
            "description": "Fecha de la factura, no de alta.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "fecha_hasta",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Una página de gastos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListaGasto"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Gastos"
        ],
        "operationId": "crearGasto",
        "summary": "Registrar un gasto",
        "description": "Permiso: `expenses:write`. La cuota de IVA y el total se calculan a partir de la base y del tipo: no se aceptan de fuera para que no puedan descuadrar con el libro de IVA. Si la empresa exige aprobación de gastos, el gasto nace pendiente igual que si se hubiera tecleado.",
        "security": [
          {
            "llaveApi": [
              "expenses:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Opcional pero muy recomendable. Una cadena única por operación (por ejemplo el id del pedido en tu tienda). Si repites la petición con la misma llave y el mismo cuerpo, se devuelve la respuesta guardada en vez de crear otra vez. La misma llave con otro cuerpo devuelve 409. Se recuerda 24 horas.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GastoNuevo"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "El gasto creado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Gasto"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "La Idempotency-Key ya se usó con otro contenido, o hay otra petición con esa misma llave todavía en marcha.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/gastos/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Gastos"
        ],
        "operationId": "verGasto",
        "summary": "Ver un gasto",
        "description": "Permiso: `expenses:read`.",
        "security": [
          {
            "llaveApi": [
              "expenses:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "El gasto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Gasto"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No existe ese recurso en esta empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/facturas": {
      "get": {
        "tags": [
          "Facturas"
        ],
        "operationId": "listarFacturas",
        "summary": "Listar facturas",
        "description": "Permiso: `invoices:read`. Devuelve **sólo facturas**; los presupuestos y albaranes viven en la misma tabla pero hay que pedirlos con `tipo_documento`. Las facturas de un listado vienen **sin la clave `lineas`**: para verlas, pide la factura suelta.",
        "security": [
          {
            "llaveApi": [
              "invoices:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "limite",
            "in": "query",
            "description": "Cuántos elementos devolver. Por defecto 50, máximo 200. Pedir más no da error: se recorta.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "desde",
            "in": "query",
            "description": "Cursor opaco. No lo construyas: cópialo del campo «siguiente» de la respuesta anterior, o sigue directamente esa URL.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "orden",
            "in": "query",
            "description": "«reciente» empieza por lo último creado (por defecto). «antiguo» empieza por lo primero, que es lo que quieres para recorrer el histórico entero. El cursor no se puede mezclar entre órdenes.",
            "schema": {
              "type": "string",
              "enum": [
                "reciente",
                "antiguo"
              ],
              "default": "reciente"
            }
          },
          {
            "name": "buscar",
            "in": "query",
            "description": "Texto libre. Busca sin distinguir mayúsculas en los campos principales del recurso.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "actualizado_desde",
            "in": "query",
            "description": "Sólo lo modificado a partir de esta fecha, contando el alta como una modificación. Es el filtro con el que se hace una sincronización incremental: guarda la hora de tu última pasada y pásala aquí en la siguiente. AVISO, y sólo la primera vez: las filas anteriores a que Cairos empezara a guardar la fecha de modificación se quedaron con la fecha de esa migración, así que tu primera sincronización las verá todas como cambiadas.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "actualizado_hasta",
            "in": "query",
            "description": "El otro extremo de la ventana. Se incluye la fecha indicada.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "estado",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "sent",
                "paid",
                "overdue"
              ]
            }
          },
          {
            "name": "serie",
            "in": "query",
            "description": "Serie de numeración exacta.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contacto_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tipo_documento",
            "in": "query",
            "description": "Por defecto «invoice».",
            "schema": {
              "type": "string",
              "enum": [
                "invoice",
                "quote",
                "delivery",
                "proforma"
              ],
              "default": "invoice"
            }
          },
          {
            "name": "emitidas_desde",
            "in": "query",
            "description": "Por fecha de emisión, que NO es la de alta.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "emitidas_hasta",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Una página de facturas, sin líneas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListaFactura"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Facturas"
        ],
        "operationId": "crearFactura",
        "summary": "Crear una factura",
        "description": "Permiso: `invoices:write`.\n\n**Por defecto crea un BORRADOR sin número.** Manda `\"emitir\": true` para emitirla en el acto, con su número reservado. El valor por defecto es el prudente a propósito: veinte borradores de prueba se borran, veinte facturas emitidas hay que rectificarlas una a una y explicar por qué.\n\nEmitir con una llave `cai_live_` registra la factura en VeriFactu y la encadena. Con una llave `cai_test_` **no**: se crea igual, con su número de su serie, pero no entra en la cadena de huellas ni se manda nada a la AEAT. Es lo que permite montar la integración de verdad sin ensuciar una serie real.\n\nUsa `Idempotency-Key`. Aquí es donde más importa: si la red se corta y reintentas sin ella, salen dos facturas con dos números para el mismo pedido, y eso no se arregla borrando.",
        "security": [
          {
            "llaveApi": [
              "invoices:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Opcional pero muy recomendable. Una cadena única por operación (por ejemplo el id del pedido en tu tienda). Si repites la petición con la misma llave y el mismo cuerpo, se devuelve la respuesta guardada en vez de crear otra vez. La misma llave con otro cuerpo devuelve 409. Se recuerda 24 horas.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FacturaNueva"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "La factura creada, con sus líneas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Factura"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "La Idempotency-Key ya se usó con otro contenido, o hay otra petición con esa misma llave todavía en marcha.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/facturas/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Facturas"
        ],
        "operationId": "verFactura",
        "summary": "Ver una factura",
        "description": "Permiso: `invoices:read`. Aquí sí vienen las `lineas` y el estado de VeriFactu.",
        "security": [
          {
            "llaveApi": [
              "invoices:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "La factura, con sus líneas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Factura"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No existe ese recurso en esta empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/facturas/{id}/emitir": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "Facturas"
        ],
        "operationId": "emitirFactura",
        "summary": "Emitir una factura que estaba en borrador",
        "description": "Permiso: `invoices:write`. Le reserva el número siguiente de su serie, ajusta el stock y —salvo en modo de pruebas— la registra en VeriFactu.\n\n**Emitir dos veces no es un error.** Si ya estaba emitida se devuelve tal cual con un `200`; si la has emitido tú con esta llamada, un `201`. Así un reintento no tiene que distinguir «ya estaba» de «ha fallado».",
        "security": [
          {
            "llaveApi": [
              "invoices:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Opcional pero muy recomendable. Una cadena única por operación (por ejemplo el id del pedido en tu tienda). Si repites la petición con la misma llave y el mismo cuerpo, se devuelve la respuesta guardada en vez de crear otra vez. La misma llave con otro cuerpo devuelve 409. Se recuerda 24 horas.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ya estaba emitida. Se devuelve como está.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Factura"
                }
              }
            }
          },
          "201": {
            "description": "Emitida ahora, ya con su número.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Factura"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No existe ese recurso en esta empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "La Idempotency-Key ya se usó con otro contenido, o hay otra petición con esa misma llave todavía en marcha.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/facturas/{id}/pdf": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Facturas"
        ],
        "operationId": "pdfFactura",
        "summary": "Descargar el PDF de una factura",
        "description": "Permiso: `invoices:read`. Devuelve el PDF, no JSON. Un borrador sale con «borrador» en el nombre del fichero en vez de un número que todavía no es suyo.",
        "security": [
          {
            "llaveApi": [
              "invoices:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "descargar",
            "in": "query",
            "description": "Con cualquier valor, el PDF llega como adjunto en vez de para verlo en el navegador.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "El documento en PDF.",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No existe ese recurso en esta empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "La Idempotency-Key ya se usó con otro contenido, o hay otra petición con esa misma llave todavía en marcha.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "No se ha podido generar ahora mismo (la cola de impresión está ocupada). Es temporal: reintenta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/cobros": {
      "get": {
        "tags": [
          "Cobros"
        ],
        "operationId": "listarCobros",
        "summary": "Listar cobros",
        "description": "Permiso: `payments:read`.",
        "security": [
          {
            "llaveApi": [
              "payments:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "limite",
            "in": "query",
            "description": "Cuántos elementos devolver. Por defecto 50, máximo 200. Pedir más no da error: se recorta.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "desde",
            "in": "query",
            "description": "Cursor opaco. No lo construyas: cópialo del campo «siguiente» de la respuesta anterior, o sigue directamente esa URL.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "orden",
            "in": "query",
            "description": "«reciente» empieza por lo último creado (por defecto). «antiguo» empieza por lo primero, que es lo que quieres para recorrer el histórico entero. El cursor no se puede mezclar entre órdenes.",
            "schema": {
              "type": "string",
              "enum": [
                "reciente",
                "antiguo"
              ],
              "default": "reciente"
            }
          },
          {
            "name": "buscar",
            "in": "query",
            "description": "Texto libre. Busca sin distinguir mayúsculas en los campos principales del recurso.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "actualizado_desde",
            "in": "query",
            "description": "Sólo lo modificado a partir de esta fecha, contando el alta como una modificación. Es el filtro con el que se hace una sincronización incremental: guarda la hora de tu última pasada y pásala aquí en la siguiente. AVISO, y sólo la primera vez: las filas anteriores a que Cairos empezara a guardar la fecha de modificación se quedaron con la fecha de esa migración, así que tu primera sincronización las verá todas como cambiadas.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "actualizado_hasta",
            "in": "query",
            "description": "El otro extremo de la ventana. Se incluye la fecha indicada.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "factura_id",
            "in": "query",
            "description": "Sólo los de esa factura.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Una página de cobros.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListaCobro"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Cobros"
        ],
        "operationId": "crearCobro",
        "summary": "Registrar un cobro",
        "description": "Permiso: `payments:write`.\n\nEl estado de la factura **se deduce de la suma de sus cobros**, no se escribe: cuando lo cobrado alcanza el total, la factura pasa sola a `paid` con la fecha del último cobro.\n\nUn importe que supere lo pendiente se rechaza con `422`. No es quisquillosidad: un cobro de más descuadra la tesorería y el libro diario, y por la API no hay nadie mirando la pantalla.",
        "security": [
          {
            "llaveApi": [
              "payments:write"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Opcional pero muy recomendable. Una cadena única por operación (por ejemplo el id del pedido en tu tienda). Si repites la petición con la misma llave y el mismo cuerpo, se devuelve la respuesta guardada en vez de crear otra vez. La misma llave con otro cuerpo devuelve 409. Se recuerda 24 horas.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CobroNuevo"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "El cobro registrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Cobro"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No existe ese recurso en esta empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "La Idempotency-Key ya se usó con otro contenido, o hay otra petición con esa misma llave todavía en marcha.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "listarWebhooks",
        "summary": "Listar los avisos dados de alta",
        "description": "Permiso: `webhooks:manage`. El secreto de firma **no** sale aquí: sólo se enseña al crearlo.",
        "security": [
          {
            "llaveApi": [
              "webhooks:manage"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Los webhooks de la empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListaWebhook"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "crearWebhook",
        "summary": "Dar de alta un aviso",
        "description": "Permiso: `webhooks:manage`. Un webhook evita que tu integración pregunte «¿hay algo nuevo?» cada minuto.\n\nSucesos disponibles: `invoice.created`, `invoice.issued`, `invoice.paid`, `payment.recorded`, `contact.created`, `product.updated`. También vale `\"*\"` para todos.\n\n**La respuesta trae el `secreto`, y es la única vez que se ve.** Con él se firma cada aviso. Comprueba la firma antes de fiarte de nada: sin comprobarla, cualquiera que averigüe tu URL puede inventarse un aviso, y una integración que se cree un aviso falso crea facturas de pedidos que no existen.",
        "security": [
          {
            "llaveApi": [
              "webhooks:manage"
            ]
          }
        ],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Opcional pero muy recomendable. Una cadena única por operación (por ejemplo el id del pedido en tu tienda). Si repites la petición con la misma llave y el mismo cuerpo, se devuelve la respuesta guardada en vez de crear otra vez. La misma llave con otro cuerpo devuelve 409. Se recuerda 24 horas.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookNuevo"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Creado. Guarda el secreto ahora.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookCreado"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Se ha llegado al tope de webhooks de la empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "verWebhook",
        "summary": "Ver un aviso",
        "description": "Permiso: `webhooks:manage`. Sin el secreto.",
        "security": [
          {
            "llaveApi": [
              "webhooks:manage"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "El webhook.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Webhook"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No existe ese recurso en esta empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "borrarWebhook",
        "summary": "Dar de baja un aviso",
        "description": "Permiso: `webhooks:manage`. Se borra de verdad, con su secreto: quien da de baja un webhook casi siempre lo hace porque sospecha que ese secreto se ha filtrado, y desactivarlo lo dejaría guardado.",
        "security": [
          {
            "llaveApi": [
              "webhooks:manage"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Borrado.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BorradoSimple"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No existe ese recurso en esta empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/{id}/probar": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "probarWebhook",
        "summary": "Mandar un aviso de prueba",
        "description": "Permiso: `webhooks:manage`. Manda un `webhook.test` a tu URL y te cuenta qué contestó, para poder ajustar el receptor sin esperar a que pase algo de verdad.",
        "security": [
          {
            "llaveApi": [
              "webhooks:manage"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Resultado del envío.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PruebaWebhook"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No existe ese recurso en esta empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/{id}/envios": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "tags": [
          "Webhooks"
        ],
        "operationId": "enviosWebhook",
        "summary": "Historial de envíos",
        "description": "Permiso: `webhooks:manage`. Qué se te ha mandado, qué contestaste y cuándo se reintentará. Es por donde se mira cuando «no llegan los avisos».",
        "security": [
          {
            "llaveApi": [
              "webhooks:manage"
            ]
          }
        ],
        "parameters": [
          {
            "name": "limite",
            "in": "query",
            "description": "Cuántos elementos devolver. Por defecto 50, máximo 200. Pedir más no da error: se recorta.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "desde",
            "in": "query",
            "description": "Cursor opaco. No lo construyas: cópialo del campo «siguiente» de la respuesta anterior, o sigue directamente esa URL.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "orden",
            "in": "query",
            "description": "«reciente» empieza por lo último creado (por defecto). «antiguo» empieza por lo primero, que es lo que quieres para recorrer el histórico entero. El cursor no se puede mezclar entre órdenes.",
            "schema": {
              "type": "string",
              "enum": [
                "reciente",
                "antiguo"
              ],
              "default": "reciente"
            }
          },
          {
            "name": "buscar",
            "in": "query",
            "description": "Texto libre. Busca sin distinguir mayúsculas en los campos principales del recurso.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "actualizado_desde",
            "in": "query",
            "description": "Sólo lo modificado a partir de esta fecha, contando el alta como una modificación. Es el filtro con el que se hace una sincronización incremental: guarda la hora de tu última pasada y pásala aquí en la siguiente. AVISO, y sólo la primera vez: las filas anteriores a que Cairos empezara a guardar la fecha de modificación se quedaron con la fecha de esa migración, así que tu primera sincronización las verá todas como cambiadas.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "actualizado_hasta",
            "in": "query",
            "description": "El otro extremo de la ventana. Se incluye la fecha indicada.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "estado",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "sent",
                "failed"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Una página de envíos.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListaEnvioWebhook"
                }
              }
            }
          },
          "401": {
            "description": "Falta la llave o no vale (inexistente, revocada o caducada).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "La llave no tiene el permiso que pide esta ruta.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No existe ese recurso en esta empresa.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Los datos o los parámetros no son válidos. «detalles.campos» dice qué campo y por qué.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Se ha pasado del límite de 120 peticiones por minuto. Espera los segundos que diga «Retry-After».",
            "headers": {
              "RateLimit-Limit": {
                "schema": {
                  "type": "integer"
                },
                "description": "Peticiones por minuto permitidas."
              },
              "RateLimit-Remaining": {
                "schema": {
                  "type": "integer"
                },
                "description": "Las que quedan en la ventana actual."
              },
              "RateLimit-Reset": {
                "schema": {
                  "type": "integer"
                },
                "description": "Segundos hasta que se libere un hueco."
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Lo mismo, para clientes que no entienden RateLimit-*."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Fallo del servidor. «detalles.referencia» identifica el error en el log.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "x-scopes": {
    "contacts:read": "Leer clientes y proveedores",
    "contacts:write": "Crear, modificar y borrar clientes y proveedores",
    "products:read": "Leer productos y servicios",
    "products:write": "Crear, modificar y borrar productos y servicios",
    "invoices:read": "Leer facturas y presupuestos",
    "invoices:write": "Emitir y modificar facturas",
    "expenses:read": "Leer gastos",
    "expenses:write": "Registrar gastos",
    "payments:read": "Leer cobros y pagos",
    "payments:write": "Registrar cobros y pagos",
    "webhooks:manage": "Dar de alta y de baja avisos automáticos"
  }
}