{"openapi":"3.1.0","info":{"title":"API de herramient.as","version":"1.0.0","summary":"Herramientas online para España, disponibles como API REST.","description":"API pública de [herramient.as](https://herramient.as).\n\nAutenticación por API key en la cabecera `Authorization: Bearer htas_...`.\nPuedes crear claves desde tu panel en https://herramient.as/dashboard.\n\nDos límites independientes. El caudal, que en el plan gratuito son\n20 peticiones por minuto y\n500 por hora, y se contesta con un `429`\ncuando se supera. Y la cuota del periodo de facturación, que en el plan\ngratuito son 2000 unidades, y se contesta\ncon un `402` cuando se agota: esperar no la arregla.\n\nLas respuestas autenticadas con una clave incluyen cabeceras `X-RateLimit-*`\ny `X-Quota-*`.\n\nLa mayoría de los endpoints cuesta una unidad. Los que hacen más trabajo\ncuestan más: consultar el Catastro son 5 unidades y extraer texto por OCR\nson 10.\n\nLos validadores comprueban el dígito o la letra de control de un identificador,\nlo que detecta erratas y números inventados. No confirman que el documento\nexista ni que pertenezca a una persona o empresa concreta: no hay acceso a\nningún registro de titulares.","contact":{"name":"Soporte","email":"hola@herramient.as","url":"https://herramient.as/api-docs"},"termsOfService":"https://herramient.as/terminos"},"servers":[{"url":"https://herramient.as","description":"Producción"}],"tags":[{"name":"hex-rgb","description":"Conversor HEX ↔ RGB"},{"name":"uuid","description":"Generador de UUID"},{"name":"iban","description":"Validador de IBAN"},{"name":"dni-nif-validator","description":"Comprobar DNI, NIF y NIE"},{"name":"comprobar-cif","description":"Comprobar CIF de empresa"},{"name":"convertir-ccc-a-iban","description":"Convertir CCC a IBAN"},{"name":"comprobar-numero-seguridad-social","description":"Comprobar número de la Seguridad Social"},{"name":"comprobar-referencia-catastral","description":"Comprobar referencia catastral"},{"name":"calculadora-hipoteca","description":"Calculadora de hipoteca"},{"name":"amortizacion-anticipada-hipoteca","description":"Amortización anticipada: reducir cuota o plazo"},{"name":"gastos-compraventa-vivienda","description":"Gastos de compraventa de vivienda"},{"name":"calculadora-itp","description":"Calculadora de ITP"},{"name":"dni-nif-generator","description":"Generador DNI/NIE"},{"name":"qr","description":"Generador de códigos QR"},{"name":"qr-wifi","description":"QR de wifi"},{"name":"qr-whatsapp","description":"QR de WhatsApp"},{"name":"qr-resenas-google","description":"QR de reseñas de Google"},{"name":"qr-tarjeta-contacto","description":"QR de tarjeta de contacto"},{"name":"cual-es-mi-ip","description":"¿Cuál es mi IP?"},{"name":"extraer-texto-pdf-imagen","description":"Extraer Texto de PDF o Imagen"},{"name":"ver-metadatos-foto","description":"Ver Metadatos de Foto"},{"name":"leer-codigo-qr","description":"Lector de Códigos QR"},{"name":"comprimir-imagen","description":"Comprimir Imagen"},{"name":"redimensionar-imagen","description":"Redimensionar Imagen"},{"name":"recortar-imagen-redes-sociales","description":"Recortar Imagen para Redes"},{"name":"comparar-textos","description":"Comparar Dos Textos"},{"name":"identificadores","description":"Endpoints que cubren varios tipos de identificador"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"API key con prefijo htas_. Créala en https://herramient.as/dashboard."}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"string","description":"Mensaje de error legible"},"details":{"type":"array","items":{"type":"string"},"description":"Detalles adicionales, cuando existen"}},"required":["error"]},"QuotaError":{"type":"object","properties":{"error":{"type":"string","description":"Mensaje de error legible"},"used":{"type":"integer","description":"Unidades consumidas en el periodo"},"allowance":{"type":"integer","description":"Unidades incluidas en el plan"},"reset_at":{"type":"string","format":"date","description":"Fecha en la que se renueva la cuota"},"upgrade_url":{"type":"string","description":"Dónde ampliar el plan"}},"required":["error","used","allowance","reset_at"]}},"responses":{"Unauthorized":{"description":"Falta la API key o no es válida","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Se ha superado el límite de peticiones","headers":{"Retry-After":{"description":"Segundos que hay que esperar antes de reintentar","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"QuotaExhausted":{"description":"Se ha agotado la cuota del periodo de facturación del workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuotaError"}}}}}},"paths":{"/api/v1/tools/hex_to_rgb":{"post":{"operationId":"hexToRgb","summary":"Convierte un color HEX a RGB","tags":["hex-rgb"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"r":{"type":"integer","description":"Componente rojo, 0-255"},"g":{"type":"integer","description":"Componente verde, 0-255"},"b":{"type":"integer","description":"Componente azul, 0-255"},"hex":{"type":"string","description":"El color normalizado en mayúsculas"}}}}}},"400":{"description":"Formato HEX inválido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"hex":{"type":"string","description":"Color en formato #RRGGBB"}},"required":["hex"]},"example":{"hex":"#FF5733"}}}}}},"/api/v1/tools/rgb_to_hex":{"post":{"operationId":"rgbToHex","summary":"Convierte un color RGB a HEX","tags":["hex-rgb"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"hex":{"type":"string","description":"Color en formato #RRGGBB"},"r":{"type":"integer","description":"Componente rojo recibido"},"g":{"type":"integer","description":"Componente verde recibido"},"b":{"type":"integer","description":"Componente azul recibido"}}}}}},"400":{"description":"Valores RGB fuera del rango 0-255","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"r":{"type":"integer","description":"Componente rojo, 0-255"},"g":{"type":"integer","description":"Componente verde, 0-255"},"b":{"type":"integer","description":"Componente azul, 0-255"}},"required":["r","g","b"]},"example":{"r":255,"g":87,"b":51}}}}}},"/api/v1/tools/uuid":{"post":{"operationId":"generateUuid","summary":"Genera uno o varios UUID v4","tags":["uuid"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"uuids":{"type":"array","description":"Los UUID generados","items":{"type":"string"}},"count":{"type":"integer","description":"Cuántos se han generado realmente"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Los valores de count fuera del rango 1-10 se ajustan al límite más cercano en lugar de devolver un error.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer","description":"Cuántos UUID generar, de 1 a 10. Por defecto 1"}}},"example":{"count":3}}}}}},"/api/v1/tools/iban/validate":{"post":{"operationId":"validateIban","summary":"Valida un IBAN de cualquier país SEPA","tags":["iban"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean","description":"Si el IBAN es válido"},"country":{"type":"string","description":"Código de país ISO"},"formatted":{"type":"string","description":"IBAN normalizado"},"bank_code":{"type":"string","description":"Código de entidad"},"branch_code":{"type":"string","description":"Código de oficina"},"account_number":{"type":"string","description":"Número de cuenta"},"check_digits":{"type":"string","description":"Dígitos de control del IBAN"},"errors":{"type":"object","description":"Detalle de los fallos cuando valid es false"},"message":{"type":"string","description":"Mensaje legible cuando valid es false"}}}}}},"400":{"description":"El IBAN está vacío","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"iban":{"type":"string","description":"IBAN completo, con código de país"}},"required":["iban"]},"example":{"iban":"ES9121000418450200051332"}}}}}},"/api/v1/tools/dni/validate":{"post":{"operationId":"validateDni","summary":"Valida un DNI, NIE o NIF","tags":["dni-nif-validator"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean","description":"Si el documento es válido"},"type":{"type":"string","description":"Tipo detectado, null si no se reconoce","enum":["DNI","NIE","NIF"]},"checksum_valid":{"type":"boolean","description":"Si la letra o carácter de control cuadra"},"message":{"type":"string","description":"Mensaje legible del resultado"}}}}}},"400":{"description":"El documento está vacío","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Detecta automáticamente el tipo de documento. Por compatibilidad histórica los identificadores de entidad se devuelven con type NIF; usa /api/v1/tools/identifier/detect si prefieres el tipo preciso y el desglose.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"DNI, NIE o NIF a validar"}},"required":["id"]},"example":{"id":"12345678Z"}}}}}},"/api/v1/tools/cif/validate":{"post":{"operationId":"validateCif","summary":"Valida el CIF de una entidad y describe su naturaleza jurídica","tags":["comprobar-cif"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean","description":"Si el CIF es válido"},"type":{"type":"string","description":"CIF, o null si el formato no se reconoce"},"checksum_valid":{"type":"boolean","description":"Si el carácter de control cuadra con los dígitos"},"normalized":{"type":"string","description":"Entrada en mayúsculas y sin separadores"},"details":{"type":"object","description":"key, entity_key, control_type (letter o digit), expected_control y given_control"},"message":{"type":"string","description":"Mensaje legible del resultado"}}}}}},"400":{"description":"El CIF está vacío","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"El carácter de control es un dígito para las claves A, B, C, D, E, F, G, H, J, U y V, y una letra para N, P, Q, R, S y W. Ninguna clave admite las dos formas.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"cif":{"type":"string","description":"CIF de la entidad"}},"required":["cif"]},"example":{"cif":"A28015865"}}}}}},"/api/v1/tools/ccc/to_iban":{"post":{"operationId":"cccToIban","summary":"Convierte un CCC de 20 dígitos en su IBAN","tags":["convertir-ccc-a-iban"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean","description":"Si el CCC es válido"},"type":{"type":"string","description":"CCC, o null si el formato no se reconoce"},"checksum_valid":{"type":"boolean","description":"Si los dígitos de control cuadran"},"normalized":{"type":"string","description":"Entrada sin separadores"},"details":{"type":"object","description":"entidad, oficina, control, cuenta, expected_control e iban (electronic y formatted), null si no es válido"},"message":{"type":"string","description":"Mensaje legible del resultado"}}}}}},"400":{"description":"El CCC está vacío","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Valida primero los dos dígitos de control internos del CCC. Si no cuadran no se devuelve IBAN, porque construirlo sobre una cuenta incorrecta produciría un IBAN sintácticamente válido que apunta a otra cuenta.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ccc":{"type":"string","description":"Código Cuenta Cliente de 20 dígitos. Se admiten espacios y guiones"}},"required":["ccc"]},"example":{"ccc":"21000418450200051332"}}}}}},"/api/v1/tools/naf/validate":{"post":{"operationId":"validateNaf","summary":"Valida un número de afiliación a la Seguridad Social (NAF o NUSS)","tags":["comprobar-numero-seguridad-social"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean","description":"Si el número es válido: control correcto y provincia existente"},"type":{"type":"string","description":"NAF, o null si el formato no se reconoce"},"checksum_valid":{"type":"boolean","description":"Si los dígitos de control cuadran, con independencia de la provincia"},"normalized":{"type":"string","description":"Entrada sin separadores"},"details":{"type":"object","description":"province, province_key, province_name, serial, control y expected_control"},"message":{"type":"string","description":"Mensaje legible del resultado"}}}}}},"400":{"description":"El número de la Seguridad Social está vacío","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Los dos dígitos de control son el resto de dividir entre 97 los diez primeros dígitos leídos en el orden en que se escriben, provincia primero. No hay paso de 98 menos el resto como en IBAN, así que el control va de 00 a 96. Se comprueba además que la provincia exista: los códigos 54 a 65 no están asignados, de modo que un número puede cuadrar aritméticamente y ser imposible.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"naf":{"type":"string","description":"Número de 12 dígitos. Se admiten espacios y guiones"}},"required":["naf"]},"example":{"naf":"081304526390"}}}}}},"/api/v1/tools/catastral/lookup":{"post":{"operationId":"lookupCatastral","summary":"Consulta una referencia catastral en el Catastro","tags":["comprobar-referencia-catastral"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"found":{"type":"boolean","description":"Si el Catastro ha resuelto la referencia"},"reference":{"type":"string","description":"La referencia completa de 20 caracteres tal y como la devuelve el Catastro"},"nature":{"type":"string","description":"Urbana o rústica, según el Catastro","enum":["urban","rural"]},"address":{"type":"string","description":"Localización completa del inmueble"},"province":{"type":"string","description":"Provincia"},"municipality":{"type":"string","description":"Municipio"},"postal_code":{"type":"string","description":"Código postal"},"use":{"type":"string","description":"Uso principal, por ejemplo Residencial o Agrario"},"built_area_m2":{"type":"integer","description":"Superficie construida del inmueble, en metros cuadrados"},"year_built":{"type":"integer","description":"Año de construcción, null si el Catastro no lo recoge"},"participation_coefficient":{"type":"number","description":"Coeficiente de participación en la finca"},"constructions":{"type":"array","description":"Desglose por unidad: use, block, floor, door y area_m2"},"plot_area_m2":{"type":"integer","description":"Superficie de la parcela en metros cuadrados, según la cartografía INSPIRE. No es la del inmueble"},"built_on_plot_m2":{"type":"integer","description":"Suma de la superficie construida de todos los edificios de la parcela. En un piso es mucho mayor que built_area_m2"},"buildings_count":{"type":"integer","description":"Número de edificios levantados sobre la parcela"},"dwellings":{"type":"integer","description":"Número de viviendas en la parcela"},"plan":{"type":"object","description":"Plano catastral: url del PNG, width, height, bbox (min_lon, min_lat, max_lon, max_lat) y outline con el contorno de la parcela en pares [lat, lon]"},"coordinates":{"type":"object","description":"lat, lon y srs del centroide de la parcela. null si el Catastro no las sirve"},"catastro_url":{"type":"string","description":"Enlace a la ficha oficial en la Sede Electrónica del Catastro"},"cartography_url":{"type":"string","description":"Enlace a la parcela en la cartografía catastral oficial"},"message":{"type":"string","description":"Mensaje legible del resultado"}}}}}},"400":{"description":"La referencia catastral está vacía","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"El servicio del Catastro no está disponible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Resuelve una referencia catastral contra los servicios web libres de la Dirección General del Catastro y devuelve los datos no protegidos del inmueble: dirección, uso, superficie construida, año de construcción, el desglose de construcciones y las coordenadas de la parcela. Añade además la superficie de la parcela, lo edificado sobre ella y el plano catastral, tomados de los servicios INSPIRE; esas tres superficies miden cosas distintas y no deben sumarse. No se consultan datos de titularidad, que requieren certificado digital. Admite la referencia completa de 20 caracteres y la parcela de 14. Si los caracteres de control no cuadran, el Catastro lo indica y la respuesta llega con found a false.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reference":{"type":"string","description":"Referencia catastral de 20 caracteres, o parcela de 14. Se admiten espacios"}},"required":["reference"]},"example":{"reference":"9872023VH5797S0001WX"}}}}}},"/api/v1/tools/catastral/search":{"post":{"operationId":"searchCatastralByAddress","summary":"Busca la referencia catastral a partir de una dirección","tags":["comprobar-referencia-catastral"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"found":{"type":"boolean","description":"true solo cuando la dirección identifica un único inmueble"},"properties":{"type":"array","description":"Cuando la dirección agrupa varios inmuebles: reference y address de cada uno"},"reference":{"type":"string","description":"La referencia completa de 20 caracteres, si hay un único inmueble"},"address":{"type":"string","description":"Localización completa, con planta y puerta"},"built_area_m2":{"type":"integer","description":"Superficie construida en metros cuadrados"},"year_built":{"type":"integer","description":"Año de construcción"},"use":{"type":"string","description":"Uso principal"},"coordinates":{"type":"object","description":"lat, lon y srs del centroide de la parcela"},"catastro_url":{"type":"string","description":"Enlace a la ficha oficial en la Sede Electrónica"},"cartography_url":{"type":"string","description":"Enlace a la parcela en la cartografía catastral oficial"},"catastro_error":{"type":"object","description":"code y description del Catastro cuando rechaza la dirección"},"message":{"type":"string","description":"Mensaje legible del resultado"}}}}}},"400":{"description":"Faltan provincia, municipio, calle o número","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"El servicio del Catastro no está disponible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"El camino inverso: de dirección a referencia catastral. Un número de calle suele agrupar muchos inmuebles, así que la respuesta habitual es una lista en properties con la referencia y la planta y puerta de cada uno; eso no es un error. Añadiendo planta y puerta la respuesta se concreta en un solo inmueble, con found a true y todos sus datos. Los nombres de provincia, municipio y calle tienen que coincidir con la grafía del Catastro: el endpoint libre no permite listar su callejero, así que cuando algo no cuadra devolvemos el motivo exacto del Catastro en catastro_error.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"province":{"type":"string","description":"Provincia, en la grafía del Catastro"},"municipality":{"type":"string","description":"Municipio"},"street_type":{"type":"string","description":"Sigla de la vía: CL, AV, PZ, CR..."},"street":{"type":"string","description":"Nombre de la vía, sin la sigla"},"number":{"type":"string","description":"Número de policía"},"block":{"type":"string","description":"Bloque"},"stairway":{"type":"string","description":"Escalera"},"floor":{"type":"string","description":"Planta. Acota la búsqueda a un inmueble"},"door":{"type":"string","description":"Puerta. Acota la búsqueda a un inmueble"}},"required":["province","municipality","street","number"]},"example":{"province":"MADRID","municipality":"MADRID","street_type":"CL","street":"ARTURO SORIA","number":"100","block":"","stairway":"","floor":"01","door":"A"}}}}}},"/api/v1/tools/catastral/municipios":{"get":{"operationId":"listCatastralMunicipios","summary":"Lista los municipios de una provincia con la grafía del Catastro","tags":["comprobar-referencia-catastral"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"municipalities":{"type":"array","description":"Nombres de municipio, alfabéticamente","items":{"type":"string"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Pensado para montar un selector antes de llamar a /catastral/search. Los nombres son los que usa el Catastro, que no siempre coinciden con los del INE. Devuelve una lista vacía para Álava, Gipuzkoa, Bizkaia y Navarra, que tienen catastro foral propio y no están en este servicio.","parameters":[{"name":"province","in":"query","required":true,"description":"Provincia, en la grafía del Catastro","schema":{"type":"string","description":"Provincia, en la grafía del Catastro"}}]}},"/api/v1/tools/catastral/vias":{"get":{"operationId":"listCatastralVias","summary":"Busca vías de un municipio con la grafía del Catastro","tags":["comprobar-referencia-catastral"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"streets":{"type":"array","description":"street_type (sigla), name (el valor para search) y label (legible)"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"El Catastro guarda los nombres de vía normalizados, sin tildes y a veces con el orden invertido, de modo que no se pueden deducir del nombre de la placa. Este endpoint los devuelve tal cual, para usarlos en el parámetro street de /catastral/search. Hacen falta al menos 3 caracteres en q y se devuelven como máximo 50 resultados.","parameters":[{"name":"province","in":"query","required":true,"description":"Provincia","schema":{"type":"string","description":"Provincia"}},{"name":"municipality","in":"query","required":true,"description":"Municipio","schema":{"type":"string","description":"Municipio"}},{"name":"q","in":"query","required":true,"description":"Parte del nombre de la vía, mínimo 3 caracteres","schema":{"type":"string","description":"Parte del nombre de la vía, mínimo 3 caracteres"}}]}},"/api/v1/tools/catastral/plano":{"get":{"operationId":"getCatastralPlano","summary":"Devuelve el plano catastral de una parcela en PNG","tags":["comprobar-referencia-catastral"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"(binario)":{"type":"string","description":"La imagen PNG. El Content-Type es image/png"}}}}}},"404":{"description":"La parcela no tiene cartografía disponible","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Responde con una imagen PNG, no con JSON: es la cartografía catastral oficial recortada sobre la parcela de la referencia. Hacemos de intermediarios porque el servicio WMS del Catastro no envía cabeceras CORS y porque el encuadre hay que calcularlo a partir de la geometría de la parcela. La atribución a la Dirección General del Catastro va impresa en la propia imagen. Devuelve 404 cuando la parcela no tiene cartografía, lo que incluye Álava, Gipuzkoa, Bizkaia y Navarra. Para situar el plano o dibujar encima, el bbox y el contorno vienen en el campo plan de /catastral/lookup.","parameters":[{"name":"reference","in":"query","required":true,"description":"Referencia catastral de 20 caracteres, o parcela de 14","schema":{"type":"string","description":"Referencia catastral de 20 caracteres, o parcela de 14"}},{"name":"tamano","in":"query","required":false,"description":"Tamaño de la imagen: card (640x420) o wide (1024x420). Por defecto card","schema":{"type":"string","description":"Tamaño de la imagen: card (640x420) o wide (1024x420). Por defecto card","enum":["card","wide"]}}]}},"/api/v1/tools/identifier/detect":{"post":{"operationId":"detectIdentifier","summary":"Detecta y valida cualquier identificador español","tags":["identificadores"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"valid":{"type":"boolean","description":"Si el identificador es válido"},"type":{"type":"string","description":"Tipo detectado, null si no se reconoce","enum":["DNI","NIE","CIF","NIF","NAF"]},"checksum_valid":{"type":"boolean","description":"Si el carácter de control cuadra"},"normalized":{"type":"string","description":"Entrada en mayúsculas y sin separadores"},"details":{"type":"object","description":"Desglose propio del tipo detectado"},"message":{"type":"string","description":"Mensaje legible del resultado"}}}}}},"400":{"description":"El identificador está vacío","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Un solo endpoint para DNI, NIE, CIF, los NIF personales K, L y M y el número de afiliación a la Seguridad Social (NAF). Devuelve el tipo detectado y el desglose propio de ese tipo.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"string","description":"El identificador a comprobar"}},"required":["value"]},"example":{"value":"A28015865"}}}}}},"/api/v1/tools/mortgage/schedule":{"post":{"operationId":"mortgageSchedule","summary":"Calcula la cuota y el cuadro de amortización de una hipoteca","tags":["calculadora-hipoteca"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","description":"Tipo de hipoteca calculada"},"months":{"type":"integer","description":"Número total de cuotas"},"capital":{"type":"number","description":"Capital prestado, en euros"},"monthly_payment":{"type":"number","description":"Primera cuota mensual, en euros"},"total_interest":{"type":"number","description":"Intereses totales a lo largo de la vida del préstamo"},"total_paid":{"type":"number","description":"Capital más intereses"},"initial_rate":{"type":"number","description":"Tipo anual aplicado a la primera cuota"},"nominal_annual_rate":{"type":"number","description":"TIN. El mismo valor que initial_rate, nombrado explícitamente"},"effective_annual_rate":{"type":"number","description":"Efecto de la capitalización mensual sobre el TIN: (1+TIN/12)^12−1. No es la TAE: no incluye gastos ni comisiones"},"revisions":{"type":"array","description":"Meses en los que cambia la cuota, con month, rate y monthly_payment"},"schedule":{"type":"array","description":"Solo con include_schedule: month, rate, payment, interest, principal y balance de cada cuota"},"disclaimer":{"type":"string","description":"Aviso de que el cálculo es orientativo"}}}}}},"400":{"description":"Capital, plazo o tipo de hipoteca no válidos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Sistema francés, con i igual al TIN dividido entre doce, que es la convención española. El redondeo es a céntimos en cada periodo y la última cuota absorbe el resto, así que el capital amortizado suma exactamente el prestado. En las hipotecas variables y mixtas la cuota se recalcula sobre el capital pendiente y el plazo restante en cada revisión, nunca sobre el capital inicial. euribor_shock permite montar escenarios de estrés: los puntos se suman al Euríbor a partir de la primera revisión, no desde el primer mes, porque el tipo inicial está fijado por contrato hasta entonces. El cuadro completo solo se devuelve si pides include_schedule.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"capital":{"type":"number","description":"Importe del préstamo en euros"},"years":{"type":"integer","description":"Plazo en años, de 1 a 50"},"type":{"type":"string","description":"Tipo de hipoteca. Por defecto fixed","enum":["fixed","variable","mixed"]},"rate":{"type":"number","description":"TIN en porcentaje. Obligatorio en fixed, y es el tramo fijo en mixed"},"euribor":{"type":"number","description":"Euríbor de referencia en porcentaje, para el tramo variable"},"differential":{"type":"number","description":"Diferencial sobre el Euríbor en puntos"},"revision_months":{"type":"integer","description":"Cada cuántos meses se revisa el tipo. Por defecto 12"},"fixed_years":{"type":"integer","description":"Años del tramo fijo, solo en mixed"},"euribor_shock":{"type":"number","description":"Puntos que se suman al Euríbor desde la primera revisión"},"include_schedule":{"type":"boolean","description":"Devuelve el cuadro mes a mes. Por defecto false"}},"required":["capital","years"]},"example":{"capital":200000,"years":25,"type":"fixed","rate":3.0,"euribor":2.143,"differential":0.9,"revision_months":12,"fixed_years":10,"euribor_shock":1,"include_schedule":false}}}}}},"/api/v1/tools/mortgage/early_repayment":{"post":{"operationId":"mortgageEarlyRepayment","summary":"Compara reducir cuota con reducir plazo al amortizar anticipadamente","tags":["amortizacion-anticipada-hipoteca"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"amount":{"type":"number","description":"Importe amortizado"},"commission":{"type":"number","description":"Comisión en euros"},"total_outlay":{"type":"number","description":"Lo que sale de tu bolsillo: importe más comisión"},"current":{"type":"object","description":"Situación actual: monthly_payment, months y total_interest"},"reduce_payment":{"type":"object","description":"Reducir cuota: lo anterior más net_saving y monthly_saving"},"reduce_term":{"type":"object","description":"Reducir plazo: lo anterior más net_saving y months_saved"},"extra_saving_from_term":{"type":"number","description":"Cuánto más ahorras reduciendo plazo en lugar de cuota"},"investment":{"type":"object","description":"Solo con investment_return: final_value, gross_gain y beats_repayment. Bruto y antes de impuestos"},"verdict":{"type":"string","description":"reduce_term, o not_worth_it si la comisión anula el ahorro","enum":["reduce_term","not_worth_it"]},"message":{"type":"string","description":"El veredicto en lenguaje llano"},"disclaimer":{"type":"string","description":"Aviso de que el cálculo es orientativo"}}}}}},"400":{"description":"Capital pendiente, plazo o importe no válidos, o importe mayor que el capital pendiente","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Devuelve las dos salidas una al lado de la otra: misma fecha de fin con cuota más baja, o misma cuota terminando antes. Reducir plazo ahorra siempre al menos lo mismo que reducir cuota, porque deja menos capital pendiente durante menos tiempo; eso es una identidad aritmética, no un hallazgo, y el campo verdict solo cambia a not_worth_it cuando la comisión se come el ahorro. La comisión se cobra sobre el importe amortizado y se suma al desembolso: el importe completo reduce capital y la comisión es dinero perdido, que es como la cobran los bancos. Los ahorros vienen ya netos de comisión.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"balance":{"type":"number","description":"Capital pendiente en euros"},"rate":{"type":"number","description":"TIN actual en porcentaje"},"remaining_months":{"type":"integer","description":"Cuotas que quedan por pagar"},"amount":{"type":"number","description":"Importe a amortizar, no mayor que el capital pendiente"},"commission_percent":{"type":"number","description":"Comisión por amortización anticipada, en porcentaje sobre el importe. Por defecto 0"},"investment_return":{"type":"number","description":"Rentabilidad anual con la que comparar invertir ese dinero en lugar de amortizar. Bruta y antes de impuestos"}},"required":["balance","rate","remaining_months","amount"]},"example":{"balance":150000,"rate":3.0,"remaining_months":200,"amount":20000,"commission_percent":0.15,"investment_return":4.0}}}}}},"/api/v1/tools/property/purchase_costs":{"post":{"operationId":"propertyPurchaseCosts","summary":"Calcula el efectivo necesario para comprar una vivienda en España","tags":["gastos-compraventa-vivienda"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"available":{"type":"boolean","description":"false cuando no hay regla verificada para esa jurisdicción y fecha. En ese caso no se devuelve ni base ni tipo ni totales"},"jurisdiction":{"type":"object","description":"code, name, type, indirect_tax_regime y foral"},"taxable_base":{"type":"object","description":"amount, source (declared_value, consideration o reference_value), above_price, below_reference y los componentes"},"acquisition_tax":{"type":"object","description":"El impuesto de adquisición: tax, rule, rate, base, quota, payer, verified_at y sources"},"ajd":{"type":"object","description":"AJD de la escritura de compraventa. Solo en obra nueva: en segunda mano es incompatible con el ITP (art. 31.2)"},"mortgage_ajd":{"type":"object","description":"AJD de la escritura de hipoteca, con payer igual a lender. No entra en el efectivo necesario"},"fees":{"type":"array","description":"Notaría, registro, gestoría y tasación, cada una con min, max, basis (legal_tariff o market) y bucket"},"totals":{"type":"object","description":"down_payment, taxes, fees_min, fees_max, cash_needed_min, cash_needed_max y paid_by_lender"},"candidates":{"type":"array","description":"Tipos reducidos que podrían aplicar pero que no se pueden confirmar, con lo que falta por saber y el ahorro potencial"},"trace":{"type":"array","description":"Cada regla considerada con su veredicto: met, unmet o unknown. Es la explicación de por qué se aplicó la que se aplicó"},"warnings":{"type":"array","description":"Avisos con code y message. Incluye los supuestos asumidos, como que no te aplica un recargo"},"disclaimer":{"type":"string","description":"Aviso de que el cálculo es orientativo"}}}}}},"400":{"description":"Jurisdicción desconocida, precio no válido o fecha mal formada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Impuesto de adquisición, honorarios y entrada, con la asignación de quién paga qué. Tres cosas que lo diferencian de multiplicar el precio por un tipo. Primero, la base imponible no es el precio: es la mayor de tres magnitudes —valor declarado, precio pactado y valor de referencia del Catastro— según el art. 10 del texto refundido del ITPAJD. Segundo, la notaría y el registro se calculan con el arancel oficial (RD 1426/1989 y RD 1427/1989, con la rebaja del 5 % y el IVA), y se devuelven como horquilla porque el arancel es el suelo de la factura. Tercero, el AJD de la escritura de hipoteca se devuelve con payer igual a lender: lo paga el banco desde el RDL 17/2018, y no entra en el efectivo que necesitas. Una jurisdicción sin tipo verificado responde 200 con available en false y el aviso correspondiente, nunca un tipo aproximado ni una media nacional.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"jurisdiction":{"type":"string","description":"Código de la comunidad, territorio foral o ciudad autónoma. Por ejemplo cataluna, galicia, madrid, navarra, bizkaia, ceuta"},"price":{"type":"number","description":"Precio de compra en euros"},"condition":{"type":"string","description":"Segunda mano o obra nueva. Por defecto second_hand","enum":["second_hand","new_build"]},"reference_value":{"type":"number","description":"Valor de referencia del Catastro en euros. Si supera el precio, pasa a ser la base imponible"},"declared_value":{"type":"number","description":"Valor declarado en la escritura, si difiere del precio. Por defecto igual al precio"},"consideration":{"type":"number","description":"Contraprestación pactada, si difiere del valor declarado. Por defecto igual al precio"},"mortgage_amount":{"type":"number","description":"Importe de la hipoteca en euros. Determina la entrada y añade la tasación"},"accrual_date":{"type":"string","description":"Fecha de devengo en AAAA-MM-DD. Selecciona la versión de la regla vigente ese día, no la de hoy. Por defecto hoy"},"use":{"type":"string","description":"Uso de la vivienda. Requisito de casi todos los tipos reducidos","enum":["habitual","other"]},"age":{"type":"integer","description":"Edad de quien compra en la fecha de devengo"},"irpf_qualifying_base":{"type":"number","description":"Base imponible general más la del ahorro de la última declaración, menos el mínimo personal y familiar. No es el sueldo bruto"},"family_category":{"type":"string","description":"Situación familiar que da derecho a tipo reducido","enum":["numerosa","monoparental"]},"extra_children":{"type":"integer","description":"Hijos por encima del mínimo de familia numerosa. Amplía el límite de renta"},"disability_percentage":{"type":"number","description":"Grado de discapacidad en porcentaje"},"gender_violence_victim":{"type":"boolean","description":"Víctima de violencia de género, para el tipo reducido correspondiente"},"municipality":{"type":"string","description":"Municipio del inmueble. Necesario para los tipos rurales"},"protected_housing":{"type":"string","description":"Calificación de vivienda protegida, para el IVA superreducido","enum":["regimen_especial","promocion_publica"]},"large_holder":{"type":"boolean","description":"Si quien compra es gran tenedor. Un valor ausente deja el recargo sin resolver y se avisa de ello"},"whole_building":{"type":"boolean","description":"Si se adquiere un edificio residencial completo"}},"required":["jurisdiction","price"]},"example":{"jurisdiction":"cataluna","price":300000,"condition":"second_hand","reference_value":330000,"declared_value":300000,"consideration":300000,"mortgage_amount":240000,"accrual_date":"2026-10-05","use":"habitual","age":30,"irpf_qualifying_base":25000,"family_category":"numerosa","extra_children":1,"disability_percentage":65,"gender_violence_victim":false,"municipality":"Tremp","protected_housing":"regimen_especial","large_holder":false,"whole_building":false}}}}}},"/api/v1/tools/property/itp":{"post":{"operationId":"propertyItp","summary":"Calcula solo el impuesto de adquisición de una vivienda","tags":["calculadora-itp"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"available":{"type":"boolean","description":"false si no hay regla verificada para esa jurisdicción y fecha"},"jurisdiction":{"type":"object","description":"code, name, type, indirect_tax_regime y foral"},"taxable_base":{"type":"object","description":"La base y de cuál de las tres magnitudes sale"},"acquisition_tax":{"type":"object","description":"tax, rule, rate, base, quota, verified_at y sources"},"ajd":{"type":"object","description":"Solo en obra nueva"},"candidates":{"type":"array","description":"Tipos reducidos que podrían aplicar, con lo que falta por saber"},"trace":{"type":"array","description":"Cada regla considerada con su veredicto"},"filing":{"type":"object","description":"model, deadline, who, where y una nota: modelo 600 y un mes desde la escritura"},"warnings":{"type":"array","description":"Avisos con code y message"},"disclaimer":{"type":"string","description":"Aviso de que el cálculo es orientativo"}}}}}},"400":{"description":"Jurisdicción desconocida, precio no válido o fecha mal formada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"El mismo motor que purchase_costs, recortado al impuesto: devuelve la base imponible, el tipo, la cuota y los datos de presentación, sin honorarios ni efectivo necesario. En segunda mano es ITP; en obra nueva, el impuesto indirecto que corresponda al territorio —IVA en la península y Baleares, IGIC en Canarias, IPSI en Ceuta y Melilla— más el AJD. Acepta los mismos parámetros de perfil que purchase_costs.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"jurisdiction":{"type":"string","description":"Código de la jurisdicción fiscal"},"price":{"type":"number","description":"Precio de compra en euros"},"condition":{"type":"string","description":"Por defecto second_hand","enum":["second_hand","new_build"]},"reference_value":{"type":"number","description":"Valor de referencia del Catastro en euros"},"accrual_date":{"type":"string","description":"Fecha de devengo en AAAA-MM-DD"},"use":{"type":"string","description":"Uso de la vivienda","enum":["habitual","other"]},"age":{"type":"integer","description":"Edad de quien compra"},"irpf_qualifying_base":{"type":"number","description":"Base de IRPF a efectos del tipo reducido"},"family_category":{"type":"string","description":"Situación familiar","enum":["numerosa","monoparental"]},"disability_percentage":{"type":"number","description":"Grado de discapacidad en porcentaje"},"municipality":{"type":"string","description":"Municipio del inmueble"},"large_holder":{"type":"boolean","description":"Si quien compra es gran tenedor"}},"required":["jurisdiction","price"]},"example":{"jurisdiction":"cataluna","price":300000,"condition":"second_hand","reference_value":330000,"accrual_date":"2026-10-05","use":"habitual","age":30,"irpf_qualifying_base":25000,"family_category":"numerosa","disability_percentage":65,"municipality":"Tremp","large_holder":false}}}}}},"/api/v1/tools/property/comparison":{"post":{"operationId":"propertyComparison","summary":"Compara el mismo impuesto en todas las jurisdicciones con datos verificados","tags":["calculadora-itp"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"price":{"type":"number","description":"El precio comparado"},"condition":{"type":"string","description":"Segunda mano u obra nueva"},"jurisdictions":{"type":"array","description":"Las jurisdicciones con datos, ordenadas por cuota ascendente, cada una con su tax y su ajd"},"unavailable":{"type":"array","description":"Las jurisdicciones sin tipo verificado todavía"},"unavailable_note":{"type":"string","description":"Por qué faltan: hueco en los datos, no ausencia de impuesto"},"disclaimer":{"type":"string","description":"Aviso de que el cálculo es orientativo"}}}}}},"400":{"description":"Precio no válido o fecha mal formada","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"La misma compra y el mismo perfil en cada comunidad, territorio foral y ciudad autónoma de la que tengamos el tipo verificado, ordenadas de menos a más impuesto. Las que no aparecen van en unavailable: es un hueco en nuestros datos, no un territorio sin impuesto.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"price":{"type":"number","description":"Precio de compra en euros"},"condition":{"type":"string","description":"Por defecto second_hand","enum":["second_hand","new_build"]},"reference_value":{"type":"number","description":"Valor de referencia del Catastro en euros"},"accrual_date":{"type":"string","description":"Fecha de devengo en AAAA-MM-DD"},"use":{"type":"string","description":"Uso de la vivienda","enum":["habitual","other"]},"age":{"type":"integer","description":"Edad de quien compra"},"irpf_qualifying_base":{"type":"number","description":"Base de IRPF a efectos del tipo reducido"},"large_holder":{"type":"boolean","description":"Si quien compra es gran tenedor"}},"required":["price"]},"example":{"price":300000,"condition":"second_hand","reference_value":330000,"accrual_date":"2026-10-05","use":"habitual","age":30,"irpf_qualifying_base":25000,"large_holder":false}}}}}},"/api/v1/tools/dni/generate":{"post":{"operationId":"generateDni","summary":"Genera un DNI, NIE o NIF válido para pruebas","tags":["dni-nif-generator"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"El identificador generado"},"type":{"type":"string","description":"Tipo generado","enum":["DNI","NIE","NIF"]},"checksum_valid":{"type":"boolean","description":"Siempre true: se genera con el control correcto"}}}}}},"400":{"description":"Tipo inválido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Los números son aleatorios y no corresponden a personas ni entidades reales. Úsalos solo para pruebas y desarrollo.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","description":"Tipo a generar. Por defecto DNI","enum":["DNI","NIE","NIF"]}}},"example":{"type":"DNI"}}}}}},"/api/v1/tools/qr/generate":{"post":{"operationId":"generateQr","summary":"Genera un código QR con estilo, colores y logo, en PNG o SVG","tags":["qr"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"URL de descarga del fichero generado"},"expires_at":{"type":"string","description":"Cuándo se eliminará el fichero","format":"date-time"},"content":{"type":"string","description":"El texto codificado en el QR"},"format":{"type":"string","description":"Formato generado"},"size":{"type":"integer","description":"Ancho pedido"},"width":{"type":"integer","description":"Ancho de la imagen en píxeles"},"height":{"type":"integer","description":"Alto de la imagen en píxeles"},"warnings":{"type":"array","description":"Avisos que pueden impedir escanearlo, como poco contraste. No bloquean la generación","items":{"type":"string"}}}}}}},"400":{"description":"Faltan datos, el contenido no cabe en un QR, o una opción o un color no son válidos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"El logo no se ha podido leer como imagen","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"El servicio que procesa el logo no está disponible; reintenta en unos segundos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Módulos cuadrados, redondeados o en puntos, esquinas a juego, colores propios y un logo opcional en el centro. Por defecto lleva debajo la firma «creado con herramient.as»; mándale caption=false para quitarla. Con logo la petición va en multipart/form-data y la corrección de errores sube al nivel H, así que cabe menos texto. El fichero se almacena 24 horas y después se elimina automáticamente; las peticiones idénticas reutilizan el fichero existente y renuevan su expiración.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"content":{"type":"string","description":"Texto o URL a codificar. Máximo 2.000 caracteres"},"format":{"type":"string","description":"Formato de salida. Por defecto png","enum":["png","svg"]},"size":{"type":"integer","description":"Ancho en píxeles. Por defecto 512. Con firma, el alto es un 8 % mayor","enum":[256,512,1024,2048]},"style":{"type":"string","description":"Forma de los módulos. Por defecto rounded","enum":["square","rounded","dots"]},"eyes":{"type":"string","description":"Forma de las tres esquinas. Por defecto rounded","enum":["square","rounded","circle"]},"color":{"type":"string","description":"Color del código en hexadecimal. Por defecto #111827"},"eye_color":{"type":"string","description":"Color de las esquinas. Por defecto el de color"},"background":{"type":"string","description":"Color del fondo. Por defecto #ffffff"},"caption":{"type":"boolean","description":"Firma «creado con herramient.as» debajo del código. Por defecto true"},"icon":{"type":"string","description":"Icono para el centro del código, dibujado en vectorial. Se ignora si se manda logo","enum":["wifi","whatsapp","star","contact"]},"logo":{"type":"file","description":"Imagen PNG, JPG o WebP de hasta 5 MB para el centro del código, como multipart/form-data"},"logo_size":{"type":"string","description":"Tamaño del logo. Por defecto medium","enum":["small","medium","large"]}},"required":["content"]},"example":{"content":"https://herramient.as","format":"png","size":1024,"style":"rounded","eyes":"rounded","color":"#1e3a8a","eye_color":"#1e3a8a","background":"#ffffff","caption":true,"icon":"wifi","logo_size":"medium"}}}}}},"/api/v1/tools/qr/wifi":{"post":{"operationId":"generateWifiQr","summary":"Genera un código QR para conectarse a una red wifi","tags":["qr-wifi"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"URL de descarga del fichero generado"},"expires_at":{"type":"string","description":"Cuándo se eliminará el fichero","format":"date-time"},"content":{"type":"string","description":"El texto codificado en el QR"},"format":{"type":"string","description":"Formato generado"},"size":{"type":"integer","description":"Ancho pedido"},"width":{"type":"integer","description":"Ancho de la imagen en píxeles"},"height":{"type":"integer","description":"Alto de la imagen en píxeles"},"warnings":{"type":"array","description":"Avisos que pueden impedir escanearlo, como poco contraste. No bloquean la generación","items":{"type":"string"}}}}}}},"400":{"description":"Faltan datos, el contenido no cabe en un QR, o una opción o un color no son válidos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"El logo no se ha podido leer como imagen","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"El servicio que procesa el logo no está disponible; reintenta en unos segundos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Compone el texto WIFI:T:...;S:...;P:...;; que leen las cámaras de iPhone y Android, con los caracteres especiales del nombre y la contraseña ya escapados, y lo dibuja con las mismas opciones de diseño que /qr/generate.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ssid":{"type":"string","description":"Nombre de la red, tal cual. Máximo 32 bytes"},"password":{"type":"string","description":"Contraseña. Obligatoria salvo con security=nopass; con WPA, de 8 a 63 caracteres"},"security":{"type":"string","description":"WPA cubre WPA, WPA2 y WPA3. Por defecto WPA","enum":["WPA","WEP","nopass"]},"hidden":{"type":"boolean","description":"Si la red no emite su nombre. Por defecto false"},"format":{"type":"string","description":"Formato de salida. Por defecto png","enum":["png","svg"]},"size":{"type":"integer","description":"Ancho en píxeles. Por defecto 512. Con firma, el alto es un 8 % mayor","enum":[256,512,1024,2048]},"style":{"type":"string","description":"Forma de los módulos. Por defecto rounded","enum":["square","rounded","dots"]},"eyes":{"type":"string","description":"Forma de las tres esquinas. Por defecto rounded","enum":["square","rounded","circle"]},"color":{"type":"string","description":"Color del código en hexadecimal. Por defecto #111827"},"eye_color":{"type":"string","description":"Color de las esquinas. Por defecto el de color"},"background":{"type":"string","description":"Color del fondo. Por defecto #ffffff"},"caption":{"type":"boolean","description":"Firma «creado con herramient.as» debajo del código. Por defecto true"},"icon":{"type":"string","description":"Icono para el centro del código, dibujado en vectorial. Se ignora si se manda logo","enum":["wifi","whatsapp","star","contact"]},"logo":{"type":"file","description":"Imagen PNG, JPG o WebP de hasta 5 MB para el centro del código, como multipart/form-data"},"logo_size":{"type":"string","description":"Tamaño del logo. Por defecto medium","enum":["small","medium","large"]}},"required":["ssid"]},"example":{"ssid":"MiCasa_5G","password":"una-clave-larga","security":"WPA","hidden":false,"format":"png","size":1024,"style":"rounded","eyes":"rounded","color":"#1e3a8a","eye_color":"#1e3a8a","background":"#ffffff","caption":true,"icon":"wifi","logo_size":"medium"}}}}}},"/api/v1/tools/qr/whatsapp":{"post":{"operationId":"generateWhatsappQr","summary":"Genera un código QR que abre un chat de WhatsApp","tags":["qr-whatsapp"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"URL de descarga del fichero generado"},"expires_at":{"type":"string","description":"Cuándo se eliminará el fichero","format":"date-time"},"content":{"type":"string","description":"El texto codificado en el QR"},"format":{"type":"string","description":"Formato generado"},"size":{"type":"integer","description":"Ancho pedido"},"width":{"type":"integer","description":"Ancho de la imagen en píxeles"},"height":{"type":"integer","description":"Alto de la imagen en píxeles"},"warnings":{"type":"array","description":"Avisos que pueden impedir escanearlo, como poco contraste. No bloquean la generación","items":{"type":"string"}}}}}}},"400":{"description":"Faltan datos, el contenido no cabe en un QR, o una opción o un color no son válidos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"El logo no se ha podido leer como imagen","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"El servicio que procesa el logo no está disponible; reintenta en unos segundos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Compone un enlace wa.me con el número en formato internacional y el mensaje inicial codificado. Un número de 9 cifras sin prefijo que empieza por 6, 7, 8 o 9 se entiende español (+34).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"phone":{"type":"string","description":"Número de teléfono. Admite espacios, guiones y el prefijo con + o 00"},"message":{"type":"string","description":"Mensaje que aparece ya escrito al abrir el chat"},"format":{"type":"string","description":"Formato de salida. Por defecto png","enum":["png","svg"]},"size":{"type":"integer","description":"Ancho en píxeles. Por defecto 512. Con firma, el alto es un 8 % mayor","enum":[256,512,1024,2048]},"style":{"type":"string","description":"Forma de los módulos. Por defecto rounded","enum":["square","rounded","dots"]},"eyes":{"type":"string","description":"Forma de las tres esquinas. Por defecto rounded","enum":["square","rounded","circle"]},"color":{"type":"string","description":"Color del código en hexadecimal. Por defecto #111827"},"eye_color":{"type":"string","description":"Color de las esquinas. Por defecto el de color"},"background":{"type":"string","description":"Color del fondo. Por defecto #ffffff"},"caption":{"type":"boolean","description":"Firma «creado con herramient.as» debajo del código. Por defecto true"},"icon":{"type":"string","description":"Icono para el centro del código, dibujado en vectorial. Se ignora si se manda logo","enum":["wifi","whatsapp","star","contact"]},"logo":{"type":"file","description":"Imagen PNG, JPG o WebP de hasta 5 MB para el centro del código, como multipart/form-data"},"logo_size":{"type":"string","description":"Tamaño del logo. Por defecto medium","enum":["small","medium","large"]}},"required":["phone"]},"example":{"phone":"600 12 34 56","message":"Hola, quiero reservar mesa","format":"png","size":1024,"style":"rounded","eyes":"rounded","color":"#1e3a8a","eye_color":"#1e3a8a","background":"#ffffff","caption":true,"icon":"wifi","logo_size":"medium"}}}}}},"/api/v1/tools/qr/google_review":{"post":{"operationId":"generateGoogleReviewQr","summary":"Genera un código QR para dejar una reseña en Google","tags":["qr-resenas-google"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"URL de descarga del fichero generado"},"expires_at":{"type":"string","description":"Cuándo se eliminará el fichero","format":"date-time"},"content":{"type":"string","description":"El texto codificado en el QR"},"format":{"type":"string","description":"Formato generado"},"size":{"type":"integer","description":"Ancho pedido"},"width":{"type":"integer","description":"Ancho de la imagen en píxeles"},"height":{"type":"integer","description":"Alto de la imagen en píxeles"},"warnings":{"type":"array","description":"Avisos que pueden impedir escanearlo, como poco contraste. No bloquean la generación","items":{"type":"string"}}}}}}},"400":{"description":"Faltan datos, el contenido no cabe en un QR, o una opción o un color no son válidos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"El logo no se ha podido leer como imagen","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"El servicio que procesa el logo no está disponible; reintenta en unos segundos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Acepta el enlace de «Pedir reseñas» del Perfil de Empresa de Google o el Place ID del negocio. Solo se admiten enlaces de dominios de Google, para que no se pueda usar para crear un código de reseñas que lleve a otra parte.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"link":{"type":"string","description":"Enlace de reseñas de Google (g.page, maps.app.goo.gl, search.google.com...) o Place ID"},"format":{"type":"string","description":"Formato de salida. Por defecto png","enum":["png","svg"]},"size":{"type":"integer","description":"Ancho en píxeles. Por defecto 512. Con firma, el alto es un 8 % mayor","enum":[256,512,1024,2048]},"style":{"type":"string","description":"Forma de los módulos. Por defecto rounded","enum":["square","rounded","dots"]},"eyes":{"type":"string","description":"Forma de las tres esquinas. Por defecto rounded","enum":["square","rounded","circle"]},"color":{"type":"string","description":"Color del código en hexadecimal. Por defecto #111827"},"eye_color":{"type":"string","description":"Color de las esquinas. Por defecto el de color"},"background":{"type":"string","description":"Color del fondo. Por defecto #ffffff"},"caption":{"type":"boolean","description":"Firma «creado con herramient.as» debajo del código. Por defecto true"},"icon":{"type":"string","description":"Icono para el centro del código, dibujado en vectorial. Se ignora si se manda logo","enum":["wifi","whatsapp","star","contact"]},"logo":{"type":"file","description":"Imagen PNG, JPG o WebP de hasta 5 MB para el centro del código, como multipart/form-data"},"logo_size":{"type":"string","description":"Tamaño del logo. Por defecto medium","enum":["small","medium","large"]}},"required":["link"]},"example":{"link":"https://g.page/r/CabcdEFGH/review","format":"png","size":1024,"style":"rounded","eyes":"rounded","color":"#1e3a8a","eye_color":"#1e3a8a","background":"#ffffff","caption":true,"icon":"wifi","logo_size":"medium"}}}}}},"/api/v1/tools/qr/vcard":{"post":{"operationId":"generateVcardQr","summary":"Genera un código QR con una tarjeta de contacto (vCard 3.0)","tags":["qr-tarjeta-contacto"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"URL de descarga del fichero generado"},"expires_at":{"type":"string","description":"Cuándo se eliminará el fichero","format":"date-time"},"content":{"type":"string","description":"El texto codificado en el QR"},"format":{"type":"string","description":"Formato generado"},"size":{"type":"integer","description":"Ancho pedido"},"width":{"type":"integer","description":"Ancho de la imagen en píxeles"},"height":{"type":"integer","description":"Alto de la imagen en píxeles"},"warnings":{"type":"array","description":"Avisos que pueden impedir escanearlo, como poco contraste. No bloquean la generación","items":{"type":"string"}}}}}}},"400":{"description":"Faltan datos, el contenido no cabe en un QR, o una opción o un color no son válidos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"El logo no se ha podido leer como imagen","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"El servicio que procesa el logo no está disponible; reintenta en unos segundos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Compone una vCard 3.0 con los campos escapados y el teléfono en formato internacional. Hacen falta el nombre y al menos un teléfono o un correo.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"first_name":{"type":"string","description":"Nombre"},"last_name":{"type":"string","description":"Apellidos"},"organization":{"type":"string","description":"Empresa"},"title":{"type":"string","description":"Cargo"},"phone":{"type":"string","description":"Teléfono. Sin prefijo se entiende español"},"email":{"type":"string","description":"Correo electrónico"},"website":{"type":"string","description":"Web"},"street":{"type":"string","description":"Dirección"},"postal_code":{"type":"string","description":"Código postal"},"city":{"type":"string","description":"Ciudad"},"format":{"type":"string","description":"Formato de salida. Por defecto png","enum":["png","svg"]},"size":{"type":"integer","description":"Ancho en píxeles. Por defecto 512. Con firma, el alto es un 8 % mayor","enum":[256,512,1024,2048]},"style":{"type":"string","description":"Forma de los módulos. Por defecto rounded","enum":["square","rounded","dots"]},"eyes":{"type":"string","description":"Forma de las tres esquinas. Por defecto rounded","enum":["square","rounded","circle"]},"color":{"type":"string","description":"Color del código en hexadecimal. Por defecto #111827"},"eye_color":{"type":"string","description":"Color de las esquinas. Por defecto el de color"},"background":{"type":"string","description":"Color del fondo. Por defecto #ffffff"},"caption":{"type":"boolean","description":"Firma «creado con herramient.as» debajo del código. Por defecto true"},"icon":{"type":"string","description":"Icono para el centro del código, dibujado en vectorial. Se ignora si se manda logo","enum":["wifi","whatsapp","star","contact"]},"logo":{"type":"file","description":"Imagen PNG, JPG o WebP de hasta 5 MB para el centro del código, como multipart/form-data"},"logo_size":{"type":"string","description":"Tamaño del logo. Por defecto medium","enum":["small","medium","large"]}},"required":["first_name"]},"example":{"first_name":"Lucía","last_name":"García Pérez","organization":"Panadería La Espiga","title":"Gerente","phone":"600 12 34 56","email":"lucia@example.com","website":"https://example.com","street":"Calle Mayor 1","postal_code":"28013","city":"Madrid","format":"png","size":1024,"style":"rounded","eyes":"rounded","color":"#1e3a8a","eye_color":"#1e3a8a","background":"#ffffff","caption":true,"icon":"wifi","logo_size":"medium"}}}}}},"/api/v1/tools/ip":{"get":{"operationId":"lookupIp","summary":"Devuelve información sobre la IP del cliente","tags":["cual-es-mi-ip"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"ipv4":{"type":"string","description":"Dirección IPv4 detectada, null si no hay"},"ipv6":{"type":"string","description":"Dirección IPv6 detectada, null si no hay"},"country":{"type":"string","description":"País"},"country_code":{"type":"string","description":"Código ISO del país"},"city":{"type":"string","description":"Ciudad aproximada"},"region":{"type":"string","description":"Comunidad autónoma o región"},"postal_code":{"type":"string","description":"Código postal aproximado"},"latitude":{"type":"number","description":"Latitud aproximada"},"longitude":{"type":"number","description":"Longitud aproximada"},"asn":{"type":"string","description":"Número de sistema autónomo"},"organization":{"type":"string","description":"Organización propietaria del rango"},"isp":{"type":"string","description":"Proveedor de internet"},"connection_type":{"type":"string","description":"Tipo de conexión detectado"},"is_vpn_proxy":{"type":"boolean","description":"Si parece una VPN, proxy o hosting"},"timestamp":{"type":"string","description":"Momento de la consulta","format":"date-time"}}}}}},"500":{"description":"Error al resolver la geolocalización","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Detecta la IP real de quien llama, resolviendo correctamente proxies, CDN y balanceadores. No acepta una IP arbitraria como parámetro."}},"/api/v1/tools/documents/extract_text":{"post":{"operationId":"extractDocumentText","summary":"Extrae la capa de texto de un PDF","tags":["extraer-texto-pdf-imagen"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","description":"El texto extraído, con las páginas separadas por una línea en blanco"},"pages":{"type":"integer","description":"Páginas que tiene el documento"},"pages_read":{"type":"integer","description":"Páginas leídas de verdad, que difiere de pages si se aplicó max_pages"},"truncated":{"type":"boolean","description":"Si quedaron páginas sin leer por el límite"},"scanned":{"type":"boolean","description":"True si no hay ni una letra: el PDF es un escaneo y necesita OCR"},"characters":{"type":"integer","description":"Caracteres del texto extraído"},"words":{"type":"integer","description":"Palabras del texto extraído"}}}}}},"400":{"description":"Falta el fichero, no es un fichero, supera 25 MB, o max_pages fuera de rango","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"El PDF está dañado, o protegido con contraseña, o no es un PDF","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"El servicio de procesamiento no está disponible. Reintenta según Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Lee el texto que el PDF ya lleva dentro, que es el caso de casi cualquier factura, nómina o extracto generado por ordenador: es inmediato y exacto. **No hace OCR.** Un PDF escaneado son imágenes sin capa de texto y la respuesta lo indica con scanned=true; para esos, la página de la herramienta reconoce el texto en el navegador. El reconocimiento en servidor llegará con su propia celda, porque son minutos de CPU por documento y pide un trabajo asíncrono, no una llamada sincrónica. El fichero no se almacena.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"file":{"type":"file","description":"El PDF, como multipart/form-data. Máximo 25 MB"},"max_pages":{"type":"integer","description":"Cuántas páginas leer, de 1 a 200. Por defecto 200"}},"required":["file"]},"example":{"max_pages":50}}}}}},"/api/v1/tools/images/metadata":{"post":{"operationId":"readImageMetadata","summary":"Lee los metadatos EXIF de una foto","tags":["ver-metadatos-foto"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"present":{"type":"boolean","description":"Si la foto lleva metadatos. False es una respuesta válida, no un error"},"camera":{"type":"string","description":"Marca y modelo juntos"},"lens":{"type":"string","description":"Objetivo"},"software":{"type":"string","description":"Programa que la escribió o editó"},"taken_at":{"type":"string","description":"Fecha de la foto en ISO 8601","format":"date-time"},"width":{"type":"integer","description":"Ancho en píxeles según EXIF"},"height":{"type":"integer","description":"Alto en píxeles según EXIF"},"orientation":{"type":"integer","description":"Orientación EXIF, de 1 a 8"},"aperture":{"type":"number","description":"Número f"},"shutter_speed":{"type":"number","description":"Tiempo de exposición en segundos"},"iso":{"type":"integer","description":"Sensibilidad ISO"},"focal_length":{"type":"integer","description":"Distancia focal en milímetros"},"artist":{"type":"string","description":"Autor declarado"},"copyright":{"type":"string","description":"Derechos declarados"},"gps":{"type":"object","description":"latitude, longitude y altitude. Null si la foto no lleva coordenadas. La longitud es negativa al oeste de Greenwich"}}}}}},"400":{"description":"Falta el fichero, no es un fichero o supera 25 MB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"El fichero está dañado o no es JPEG ni TIFF","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"El servicio de procesamiento no está disponible. Reintenta según Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Devuelve cámara, objetivo, fecha, parámetros de disparo y coordenadas GPS. **Admite JPEG y TIFF**, que es menos de lo que lee la página de la herramienta: ahí el reconocimiento ocurre en el navegador con una librería que además entiende PNG, HEIC, AVIF, IPTC y XMP. Si necesitas esos formatos, usa la página. La foto no se almacena.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"file":{"type":"file","description":"La foto, como multipart/form-data. JPEG o TIFF. Máximo 25 MB"}},"required":["file"]}}}}}},"/api/v1/tools/images/decode_qr":{"post":{"operationId":"decodeQr","summary":"Lee los códigos QR o de barras de una imagen","tags":["leer-codigo-qr"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"found":{"type":"boolean","description":"Si se encontró al menos un código"},"symbols":{"type":"array","description":"Cada uno con data (el contenido como texto), type (QR-Code, EAN-13, CODE-128...) y quality (la confianza que da ZBar; baja en fotos borrosas)","items":{"type":"object"}}}}}}},"400":{"description":"Falta el fichero, no es un fichero o supera 25 MB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"El fichero está dañado o no es una imagen que sepamos decodificar","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"El servicio de procesamiento no está disponible. Reintenta según Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Devuelve una lista porque una imagen puede traer varios códigos; una hoja con cuatro QR impresos es un caso corriente. Lee además códigos de barras de una dimensión (EAN-13, CODE-128 y los demás que reconoce ZBar), y el campo type dice de cuál se trata. La imagen se descodifica con libvips, así que admite JPEG, PNG, WebP, AVIF y TIFF. No se almacena.\n\nEl contenido se devuelve como texto y nada más: no se interpreta, no se sigue y no se valida como URL. Un código lo escribió quien lo generó, de modo que convertirlo en algo navegable es decisión de quien llama.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"file":{"type":"file","description":"La imagen con el código, como multipart/form-data. Máximo 25 MB"}},"required":["file"]}}}}}},"/api/v1/tools/images/compress":{"post":{"operationId":"compressImage","summary":"Recomprime una imagen para que pese menos","tags":["comprimir-imagen"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"(binario)":{"type":"string","description":"Los bytes de la imagen. El Content-Type es image/\u003cformat\u003e"}}}}}},"400":{"description":"Falta el fichero, supera 25 MB, o format/quality/max_width no son válidos","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"El fichero está dañado o no es una imagen que sepamos decodificar","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"El servicio de procesamiento no está disponible. Reintenta según Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Responde con **los bytes de la imagen**, no con JSON ni con una URL: nada se almacena en el servidor. Las medidas de salida viajan en las cabeceras X-Image-Width, X-Image-Height y X-Image-Bytes. Se eliminan todos los metadatos, incluida la ubicación GPS. Reducir el ancho con max_width es lo que de verdad aligera una foto de móvil.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"file":{"type":"file","description":"La imagen, como multipart/form-data. Máximo 25 MB"},"format":{"type":"string","description":"Formato de salida. Por defecto jpeg","enum":["jpeg","png","webp","avif"]},"quality":{"type":"integer","description":"Calidad de 1 a 100. Por defecto 80. No aplica a png"},"max_width":{"type":"integer","description":"Ancho máximo en píxeles. No agranda una imagen más pequeña"}},"required":["file"]},"example":{"format":"webp","quality":80,"max_width":1920}}}}}},"/api/v1/tools/images/resize":{"post":{"operationId":"resizeImage","summary":"Cambia el tamaño de una imagen sin deformarla","tags":["redimensionar-imagen"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"(binario)":{"type":"string","description":"Los bytes de la imagen. El Content-Type es image/\u003cformat\u003e"}}}}}},"400":{"description":"Falta el fichero, no se indicó ni width ni height, o algún parámetro no es válido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"El fichero está dañado o no es una imagen que sepamos decodificar","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"El servicio de procesamiento no está disponible. Reintenta según Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Al menos una de width o height. La imagen se mete dentro de esas medidas manteniendo la proporción y **sin agrandarla nunca**: si pides 2000 px de una imagen de 800, recibes 800, y las cabeceras X-Image-Width y X-Image-Height dicen qué tamaño salió de verdad. Responde con los bytes de la imagen y no almacena nada.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"file":{"type":"file","description":"La imagen, como multipart/form-data. Máximo 25 MB"},"width":{"type":"integer","description":"Ancho máximo en píxeles, de 1 a 10000"},"height":{"type":"integer","description":"Alto máximo en píxeles, de 1 a 10000"},"format":{"type":"string","description":"Formato de salida. Por defecto jpeg","enum":["jpeg","png","webp","avif"]},"quality":{"type":"integer","description":"Calidad de 1 a 100. Por defecto 90"}},"required":["file"]},"example":{"width":1080,"height":1080,"format":"png","quality":90}}}}}},"/api/v1/tools/images/crop":{"post":{"operationId":"cropImage","summary":"Recorta una imagen a una medida exacta","tags":["recortar-imagen-redes-sociales"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"(binario)":{"type":"string","description":"Los bytes de la imagen. El Content-Type es image/\u003cformat\u003e"}}}}}},"400":{"description":"Falta el fichero, falta width o height, o algún parámetro no es válido","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"El fichero está dañado o no es una imagen que sepamos decodificar","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"El servicio de procesamiento no está disponible. Reintenta según Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"width y height son obligatorios: la medida exacta es lo que distingue un recorte de un redimensionado. La imagen rellena esa caja y lo que sobra se recorta desde el centro. Para las medidas de cada red, la página de la herramienta las trae ya puestas. Responde con los bytes y no almacena nada.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"file":{"type":"file","description":"La imagen, como multipart/form-data. Máximo 25 MB"},"width":{"type":"integer","description":"Ancho exacto de salida, de 1 a 10000"},"height":{"type":"integer","description":"Alto exacto de salida, de 1 a 10000"},"format":{"type":"string","description":"Formato de salida. Por defecto jpeg","enum":["jpeg","png","webp","avif"]},"quality":{"type":"integer","description":"Calidad de 1 a 100. Por defecto 90"}},"required":["file","width","height"]},"example":{"width":1080,"height":1350,"format":"jpeg","quality":90}}}}}},"/api/v1/jobs":{"post":{"operationId":"createJob","summary":"Crea un trabajo asíncrono","tags":["identificadores"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Identificador del trabajo, un UUID. Es lo que se usa en las otras dos llamadas"},"operation":{"type":"string","description":"La operación pedida"},"status":{"type":"string","description":"queued al crearse; después running, done, failed o cancelled"},"progress":{"type":"integer","description":"De 0 a 100"},"expires_at":{"type":"string","description":"Cuándo se borra el resultado","format":"date-time"}}}}}},"400":{"description":"Operación desconocida, falta el fichero, un parámetro no es válido, o la URL del webhook apunta a una dirección interna","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"El servicio de procesamiento no está disponible. Reintenta según Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Para las operaciones que no caben en una petición HTTP: el reconocimiento de texto de un escaneo y la conversión de vídeo tardan minutos, no milisegundos. Responde 202 con un identificador y el trabajo se ejecuta en segundo plano.\n\nUn solo recurso para todas: lo que separa un trabajo asíncrono de uno sincrónico es cuánto tarda, no qué hace, así que no hay una API por herramienta. Consulta el estado con GET /api/v1/jobs/{id} o pide un webhook.\n\n**Esta es la única familia que almacena el fichero.** El resto devuelve el resultado en la respuesta y no guarda nada; un trabajo que tarda minutos no tiene otra forma de existir. La entrada se borra en cuanto el trabajo acaba y el resultado a las 24 horas.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"operation":{"type":"string","description":"Qué hacer con el fichero","enum":["documents.ocr","video.compress","video.convert"]},"file":{"type":"file","description":"El fichero, como multipart/form-data. Máximo 500 MB"},"webhook_url":{"type":"string","description":"URL pública a la que avisar al terminar. Tiene que ser http o https y no puede apuntar a una dirección interna o reservada"},"language":{"type":"string","description":"Solo para documents.ocr. Idioma del texto, por defecto spa"},"max_pages":{"type":"integer","description":"Solo para documents.ocr. Páginas a reconocer, de 1 a 50"},"format":{"type":"string","description":"Obligatorio para vídeo: mp4, mov, mkv, webm, gif, mp3, m4a, wav u ogg"},"quality":{"type":"string","description":"Para vídeo. Por defecto media","enum":["alta","media","baja"]},"height":{"type":"integer","description":"Para vídeo. Alto máximo en píxeles, de 144 a 2160. No agranda un vídeo más pequeño"},"mute":{"type":"boolean","description":"Para vídeo. Quita la pista de audio"}},"required":["operation","file"]},"example":{"operation":"video.convert","webhook_url":"https://tu-app.example/hooks/htas","language":"spa","max_pages":10,"format":"mp4","quality":"media","height":720}}}}}},"/api/v1/jobs/:token":{"get":{"operationId":"getJob","summary":"Consulta el estado de un trabajo","tags":["identificadores"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"El identificador del trabajo"},"status":{"type":"string","description":"queued, running, done, failed o cancelled"},"progress":{"type":"integer","description":"De 0 a 100"},"error":{"type":"string","description":"Código del motivo si falló, por ejemplo no_stream o corrupt_input"},"error_message":{"type":"string","description":"El motivo en texto, listo para mostrar"},"result":{"type":"object","description":"El resultado sin el fichero. Para OCR trae el texto; para vídeo, el formato y el tamaño"},"result_url":{"type":"string","description":"Dónde descargar el fichero, cuando hay uno y no ha caducado"},"expires_at":{"type":"string","description":"Cuándo se borra el resultado","format":"date-time"}}}}}},"404":{"description":"No existe un trabajo con ese identificador en tu workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"description":"Esta es la fuente de verdad del estado, con webhook o sin él. Mientras `status` sea queued o running conviene esperar unos segundos entre consultas; un transcode de cinco minutos sondeado cada dos segundos son 150 peticiones para decir \"sigue en curso\", y para eso está el webhook."}},"/api/v1/jobs/:token/result":{"get":{"operationId":"getJobResult","summary":"Descarga el fichero de un trabajo terminado","tags":["identificadores"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"(binario)":{"type":"string","description":"Los bytes del fichero, con su Content-Type y Content-Disposition"}}}}}},"202":{"description":"El trabajo todavía no ha terminado. Reintenta según Retry-After","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"204":{"description":"La operación no produce fichero; el resultado está en el estado del trabajo","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No existe un trabajo con ese identificador en tu workspace","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"El resultado ha caducado y se ha borrado","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"El trabajo falló","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"description":"Responde con los bytes del fichero. Los distintos \"todavía no\" tienen cada uno su código, para no obligar a adivinar: **202** si el trabajo sigue en curso (con Retry-After), **204** si la operación no produce fichero —el OCR devuelve texto, y está en el estado—, **410** si el resultado ya caducó y **422** si el trabajo falló."}},"/api/v1/tools/text/diff":{"post":{"operationId":"diffText","summary":"Compara dos textos y devuelve qué ha cambiado","tags":["comparar-textos"],"responses":{"200":{"description":"Petición correcta","content":{"application/json":{"schema":{"type":"object","properties":{"identical":{"type":"boolean","description":"Si no hay ninguna diferencia tras aplicar la normalización"},"stats":{"type":"object","description":"added, removed y equal: número de líneas de cada clase"},"parts":{"type":"array","description":"Los tramos en orden. Cada uno trae type (equal, added o removed) y las líneas en left y right","items":{"type":"object"}}}}}}},"400":{"description":"Falta left o right, no son texto, o superan los 200.000 caracteres","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"402":{"$ref":"#/components/responses/QuotaExhausted"}},"description":"Devuelve los tramos iguales, eliminados y añadidos, por líneas. El único endpoint de esta familia que no toca ficheros, así que no tiene ni límite de subida ni el 503 de los demás. Las tres opciones de normalización deciden qué cuenta como un cambio; ignore_whitespace viene activada porque es lo que se quiere casi siempre al comparar texto pegado de un PDF.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"left":{"type":"string","description":"Texto original. Máximo 200.000 caracteres. Puede ser una cadena vacía"},"right":{"type":"string","description":"Texto modificado. Máximo 200.000 caracteres. Puede ser una cadena vacía"},"ignore_case":{"type":"boolean","description":"Tratar mayúsculas y minúsculas como iguales. Por defecto false"},"ignore_whitespace":{"type":"boolean","description":"Colapsar espacios repetidos y recortar los extremos. Por defecto **true**"},"ignore_punctuation":{"type":"boolean","description":"Descartar signos de puntuación. Por defecto false"}},"required":["left","right"]},"example":{"left":"plazo de treinta días","right":"plazo de quince días"}}}}}}}}