{
  "openapi": "3.0.3",
  "info": {
    "title": "tturl",
    "version": "2.0.0",
    "description": "Servicio acortador de URLs con recolección detallada de metadata de visitas.\n\n## Autenticación Machine-to-Machine (v1)\n\nLa API de gestión (`/api/v1/...`) requiere una **API key** en la cabecera `X-API-Key`.\n\n## Deduplicación\n\n`POST /api/v1/links` **no crea un segundo acortado** si la URL destino ya existe:\n**201** al crear, **200** si ya existía.\n\n## Redirección pública\n\n`GET /{code}` es público: resuelve URLs cortas, registra visitas con metadata\n(IP, User-Agent, dispositivo, UTM, geo opcional)."
  },
  "servers": [
    {
      "url": "http://127.0.0.1:8000",
      "description": "Desarrollo local"
    },
    {
      "url": "https://tturl.co",
      "description": "Producción"
    }
  ],
  "tags": [
    {
      "name": "Health",
      "description": "Estado del servicio."
    },
    {
      "name": "Public",
      "description": "Redirección y recursos públicos."
    },
    {
      "name": "Legacy (CakePHP)",
      "description": "API compatible con CakePHP. Respuestas envueltas en `{ response: ... }` excepto `/users/token`."
    },
    {
      "name": "Links v1",
      "description": "Gestión M2M de enlaces. Requiere API key."
    },
    {
      "name": "Analytics v1",
      "description": "Estadísticas y series temporales. Requiere API key."
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "API key M2M (variable `API_KEYS`)."
      },
      "LegacyJwtAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "JWT legacy firmado con `HASHIDS_SALT` (HS256). Obtener en `POST /api/users/token`."
      }
    },
    "schemas": {
      "Health": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "example": "ok"
          },
          "service": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "environment": {
            "type": "string"
          },
          "database": {
            "type": "string",
            "enum": [
              "ok",
              "error"
            ]
          }
        }
      },
      "LegacyWrapped": {
        "description": "Cuerpo JSON raíz. En CakePHP, `serialize => 'response'` (string) emite el valor de `response` sin envolver en `{\"response\": ...}`."
      },
      "LegacyLinkEntity": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "url": {
            "type": "string"
          },
          "visit_count": {
            "type": "integer",
            "nullable": true
          },
          "created": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "modified": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "user_id": {
            "type": "integer",
            "nullable": true
          },
          "short_tt": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "LegacyCreateResponse": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "example": "https://tturl.co/Ab3xY"
          },
          "shorten": {
            "type": "string"
          },
          "url_long": {
            "type": "string"
          }
        }
      },
      "LegacyTokenRequest": {
        "type": "object",
        "required": [
          "email",
          "password"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "password": {
            "type": "string"
          }
        }
      },
      "LegacyTokenResponse": {
        "type": "object",
        "properties": {
          "user": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "nullable": true
              },
              "last_name": {
                "type": "string",
                "nullable": true
              },
              "email": {
                "type": "string"
              },
              "created": {
                "type": "string",
                "nullable": true
              },
              "modified": {
                "type": "string",
                "nullable": true
              },
              "token": {
                "type": "string"
              }
            }
          }
        }
      },
      "LegacyLinkCreate": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "LegacyViewHashRequest": {
        "type": "object",
        "required": [
          "id"
        ],
        "properties": {
          "id": {
            "type": "integer"
          },
          "short_tt": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "LegacyUpdateShortRequest": {
        "type": "object",
        "properties": {
          "short_tt": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "LegacyVisitCountRequest": {
        "type": "object",
        "properties": {
          "short_tt": {
            "type": "string"
          }
        }
      },
      "LegacyOperationResult": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "message": {
            "type": "string"
          },
          "short_tt": {
            "type": "string",
            "nullable": true
          },
          "url": {
            "type": "string",
            "nullable": true
          },
          "visit_count": {
            "type": "integer",
            "nullable": true
          }
        }
      },
      "Link": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "short_code": {
            "type": "string",
            "nullable": true
          },
          "short_url": {
            "type": "string",
            "nullable": true
          },
          "url": {
            "type": "string"
          },
          "visit_count": {
            "type": "integer"
          },
          "is_active": {
            "type": "boolean"
          },
          "is_expired": {
            "type": "boolean"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "LinkCreate": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "LinkUpdate": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "LinkList": {
        "type": "object",
        "properties": {
          "total": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Link"
            }
          }
        }
      },
      "VisitList": {
        "type": "object",
        "properties": {
          "total": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        }
      },
      "LinkStats": {
        "type": "object",
        "properties": {
          "short_code": {
            "type": "string"
          },
          "total_visits": {
            "type": "integer",
            "description": "Total oficial (`links.visit_count`: histórico CakePHP + redirects)."
          },
          "tracked_visits": {
            "type": "integer",
            "description": "Filas en `visits` con tracking detallado Django (puede ser menor)."
          },
          "unique_ips": {
            "type": "integer"
          },
          "bot_visits": {
            "type": "integer"
          },
          "human_visits": {
            "type": "integer"
          },
          "first_visit": {
            "type": "string",
            "nullable": true
          },
          "last_visit": {
            "type": "string",
            "nullable": true
          },
          "by_device_type": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BreakdownItem"
            }
          },
          "by_browser": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BreakdownItem"
            }
          },
          "by_os": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BreakdownItem"
            }
          },
          "by_country": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BreakdownItem"
            }
          },
          "by_referer_domain": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BreakdownItem"
            }
          },
          "by_day": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BreakdownItem"
            }
          }
        }
      },
      "BreakdownItem": {
        "type": "object",
        "properties": {
          "value": {},
          "count": {
            "type": "integer"
          }
        }
      },
      "LinkAnalytics": {
        "type": "object",
        "description": "Snapshot analítico completo (volumen, temporal, origen, geo, tech, calidad).",
        "additionalProperties": true
      },
      "Timeseries": {
        "type": "object",
        "properties": {
          "short_code": {
            "type": "string"
          },
          "granularity": {
            "type": "string",
            "enum": [
              "minute",
              "hour",
              "day",
              "week",
              "month"
            ]
          },
          "timezone": {
            "type": "string"
          },
          "points": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "bucket": {
                  "type": "string"
                },
                "clicks": {
                  "type": "integer"
                },
                "unique": {
                  "type": "integer"
                },
                "bots": {
                  "type": "integer"
                },
                "humans": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "ErrorMessage": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string"
          }
        }
      }
    },
    "parameters": {
      "PaginationLimit": {
        "name": "limit",
        "in": "query",
        "schema": {
          "type": "integer",
          "default": 50,
          "minimum": 1,
          "maximum": 200
        }
      },
      "PaginationOffset": {
        "name": "offset",
        "in": "query",
        "schema": {
          "type": "integer",
          "default": 0,
          "minimum": 0
        }
      },
      "VisitLimit": {
        "name": "limit",
        "in": "query",
        "schema": {
          "type": "integer",
          "default": 100,
          "minimum": 1,
          "maximum": 500
        }
      },
      "ShortCode": {
        "name": "code",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "description": "Código corto (`short_tt` o hashid)."
      },
      "LegacyHashQuery": {
        "name": "id",
        "in": "query",
        "schema": {
          "type": "string"
        },
        "description": "Código hashids del enlace."
      },
      "DateFrom": {
        "name": "date_from",
        "in": "query",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "DateTo": {
        "name": "date_to",
        "in": "query",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "Timezone": {
        "name": "timezone",
        "in": "query",
        "schema": {
          "type": "string",
          "default": "America/Bogota"
        }
      },
      "ExcludeBots": {
        "name": "exclude_bots",
        "in": "query",
        "schema": {
          "type": "boolean",
          "default": true
        }
      },
      "ExcludePreviews": {
        "name": "exclude_previews",
        "in": "query",
        "schema": {
          "type": "boolean",
          "default": true
        }
      },
      "Granularity": {
        "name": "granularity",
        "in": "query",
        "schema": {
          "type": "string",
          "enum": [
            "minute",
            "hour",
            "day",
            "week",
            "month"
          ],
          "default": "hour"
        }
      }
    },
    "responses": {
      "UnauthorizedLegacy": {
        "description": "JWT inválido o ausente.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorMessage"
            },
            "example": {
              "message": "Unauthorized"
            }
          }
        }
      },
      "UnauthorizedApiKey": {
        "description": "API key inválida o ausente.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorMessage"
            },
            "example": {
              "message": "API key inválida o ausente."
            }
          }
        }
      },
      "NotFound": {
        "description": "Recurso no encontrado.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorMessage"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/health": {
      "get": {
        "tags": [
          "Health"
        ],
        "summary": "Estado del servicio",
        "description": "Comprueba que el servicio y la base de datos responden.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        }
      }
    },
    "/{code}": {
      "get": {
        "tags": [
          "Public"
        ],
        "summary": "Redirigir enlace corto",
        "description": "Resuelve un código (hashid o `short_tt`), registra la visita con metadata y redirige (302). Bots de vista previa social reciben HTML Open Graph sin incrementar el contador.",
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "utm_source",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "utm_medium",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "utm_campaign",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect a URL destino."
          },
          "404": {
            "description": "Enlace no encontrado."
          },
          "410": {
            "description": "Enlace inactivo o expirado."
          }
        }
      }
    },
    "/api/v1/links": {
      "get": {
        "tags": [
          "Links v1"
        ],
        "summary": "Listar enlaces",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/PaginationLimit"
          },
          {
            "$ref": "#/components/parameters/PaginationOffset"
          }
        ],
        "responses": {
          "200": {
            "description": "Página de enlaces.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedApiKey"
          }
        }
      },
      "post": {
        "tags": [
          "Links v1"
        ],
        "summary": "Acortar URL (dedup)",
        "description": "201 si es nuevo; 200 si la URL ya existía.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LinkCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Enlace nuevo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Link"
                }
              }
            }
          },
          "200": {
            "description": "URL ya acortada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Link"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedApiKey"
          }
        }
      }
    },
    "/api/v1/links/{code}": {
      "get": {
        "tags": [
          "Links v1"
        ],
        "summary": "Detalle de enlace",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShortCode"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Link"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedApiKey"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "tags": [
          "Links v1"
        ],
        "summary": "Actualizar enlace",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShortCode"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LinkUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Link"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedApiKey"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": [
          "Links v1"
        ],
        "summary": "Eliminar enlace",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShortCode"
          }
        ],
        "responses": {
          "204": {
            "description": "Eliminado."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedApiKey"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/links/{code}/visits": {
      "get": {
        "tags": [
          "Links v1"
        ],
        "summary": "Listar visitas",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShortCode"
          },
          {
            "$ref": "#/components/parameters/VisitLimit"
          },
          {
            "$ref": "#/components/parameters/PaginationOffset"
          },
          {
            "$ref": "#/components/parameters/DateFrom"
          },
          {
            "$ref": "#/components/parameters/DateTo"
          },
          {
            "name": "is_bot",
            "in": "query",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "country_code",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "device_type",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "utm_campaign",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "response_type",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VisitList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedApiKey"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/links/{code}/stats": {
      "get": {
        "tags": [
          "Analytics v1"
        ],
        "summary": "Estadísticas agregadas",
        "description": "Preferir `/analytics` para dashboards completos. `total_visits` es el contador oficial (`visit_count`); `tracked_visits` cuenta filas en `visits`.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShortCode"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkStats"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedApiKey"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/links/{code}/analytics": {
      "get": {
        "tags": [
          "Analytics v1"
        ],
        "summary": "Snapshot analítico completo",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShortCode"
          },
          {
            "$ref": "#/components/parameters/DateFrom"
          },
          {
            "$ref": "#/components/parameters/DateTo"
          },
          {
            "$ref": "#/components/parameters/Timezone"
          },
          {
            "$ref": "#/components/parameters/ExcludeBots"
          },
          {
            "$ref": "#/components/parameters/ExcludePreviews"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkAnalytics"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedApiKey"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/links/{code}/analytics/timeseries": {
      "get": {
        "tags": [
          "Analytics v1"
        ],
        "summary": "Serie temporal de clics",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ShortCode"
          },
          {
            "$ref": "#/components/parameters/Granularity"
          },
          {
            "$ref": "#/components/parameters/DateFrom"
          },
          {
            "$ref": "#/components/parameters/DateTo"
          },
          {
            "$ref": "#/components/parameters/Timezone"
          },
          {
            "$ref": "#/components/parameters/ExcludeBots"
          },
          {
            "$ref": "#/components/parameters/ExcludePreviews"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Timeseries"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedApiKey"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/users/token": {
      "post": {
        "tags": [
          "Legacy (CakePHP)"
        ],
        "summary": "Login legacy (JWT)",
        "description": "Emite JWT compatible con CakePHP. Valida bcrypt `$2y$` contra tabla `users`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LegacyTokenRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token emitido.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyTokenResponse"
                }
              }
            }
          },
          "404": {
            "description": "Credenciales inválidas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorMessage"
                },
                "example": {
                  "message": "User not found"
                }
              }
            }
          }
        }
      }
    },
    "/api/links": {
      "get": {
        "tags": [
          "Legacy (CakePHP)"
        ],
        "summary": "Buscar enlace por hash",
        "security": [
          {
            "LegacyJwtAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/LegacyHashQuery"
          }
        ],
        "responses": {
          "200": {
            "description": "Lista con 0 o 1 enlace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyWrapped"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedLegacy"
          }
        }
      },
      "post": {
        "tags": [
          "Legacy (CakePHP)"
        ],
        "summary": "Crear enlace o devolver existente (dedup)",
        "description": "URL nueva → `null`. URL existente → `{ url, shorten, url_long }` (raíz JSON, sin wrapper). Asigna `user_id` desde JWT `sub`.",
        "security": [
          {
            "LegacyJwtAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LegacyLinkCreate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Creado (null) o existente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyWrapped"
                }
              }
            }
          },
          "400": {
            "description": "URL ausente.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyWrapped"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedLegacy"
          }
        }
      }
    },
    "/api/viewHash": {
      "post": {
        "tags": [
          "Legacy (CakePHP)"
        ],
        "summary": "Obtener o regenerar hash (short_tt)",
        "security": [
          {
            "LegacyJwtAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LegacyViewHashRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Hash y entidad.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyWrapped"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedLegacy"
          },
          "404": {
            "description": "Enlace no encontrado."
          }
        }
      }
    },
    "/api/updateLinks": {
      "get": {
        "tags": [
          "Legacy (CakePHP)"
        ],
        "summary": "Backfill short_tt faltantes",
        "description": "Rellena códigos desde 2024-01-01 (excluye URLs con `eva.seguro`).",
        "security": [
          {
            "LegacyJwtAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "`null` (paridad CakePHP).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyWrapped"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedLegacy"
          }
        }
      }
    },
    "/api/updateshortlinks": {
      "post": {
        "tags": [
          "Legacy (CakePHP)"
        ],
        "summary": "Actualizar URL destino por short_tt",
        "description": "Público (sin JWT), como en CakePHP.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LegacyUpdateShortRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resultado de operación.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyWrapped"
                }
              }
            }
          }
        }
      }
    },
    "/api/viewvisitcounts": {
      "post": {
        "tags": [
          "Legacy (CakePHP)"
        ],
        "summary": "Contador de visitas por short_tt",
        "description": "Público (sin JWT).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LegacyVisitCountRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Contador.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyWrapped"
                }
              }
            }
          }
        }
      }
    },
    "/api/latest-links": {
      "get": {
        "tags": [
          "Legacy (CakePHP)"
        ],
        "summary": "Últimos 10 enlaces",
        "security": [
          {
            "LegacyJwtAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Lista url/short_tt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LegacyWrapped"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedLegacy"
          }
        }
      }
    }
  }
}