{
  "openapi": "3.1.0",
  "info": {
    "title": "eFISIO — API pública",
    "version": "1.2.0",
    "summary": "Clínicas, fisioterapeutas y huecos libres de eFISIO, en JSON.",
    "description": "API de solo lectura y sin autenticación para consultar la red de clínicas de fisioterapia eFISIO en Madrid y Getafe: qué clínicas hay, qué fisioterapeutas trabajan en cada una y qué huecos quedan libres.\n\nEstá pensada para que un agente pueda responder «¿dónde y cuándo puedo tratarme?» sin scrapear HTML. Para reservar hay que enviar al paciente a https://www.efisio.es/citas.\n\nLo que NO está aquí es privado a propósito: las citas concretas, los datos de paciente y la facturación no se exponen.\n\n**Servidor MCP**: además de esta API REST hay un servidor Model Context Protocol en https://www.efisio.es/mcp (Streamable HTTP, POST) que envuelve estos mismos endpoints en quince herramientas: de «me duele» a «confirma tu cita» (orientar_tratamiento, recomendar_especialista, fisio_cerca_de_mi, primera_cita_disponible, reservar_hueco…). Para un agente suele ser la vía más directa.\n\n**Negociación de formato**: cualquier página del sitio responde en `text/markdown` o `application/json` si se pide con la cabecera `Accept` correspondiente.",
    "contact": {
      "name": "eFISIO",
      "url": "https://www.efisio.es/contacta",
      "email": "gestion@efisio.es"
    },
    "license": {
      "name": "Uso permitido para consulta y citación",
      "url": "https://www.efisio.es/aviso-legal"
    }
  },
  "servers": [
    {
      "url": "https://www.efisio.es",
      "description": "Producción"
    }
  ],
  "tags": [
    {
      "name": "clinicas",
      "description": "Red de centros"
    },
    {
      "name": "servicios",
      "description": "Catálogo y precios"
    },
    {
      "name": "fisioterapeutas",
      "description": "Profesionales"
    },
    {
      "name": "disponibilidad",
      "description": "Huecos libres de agenda"
    },
    {
      "name": "busqueda",
      "description": "Buscar contenido"
    }
  ],
  "paths": {
    "/api/clinicas": {
      "get": {
        "operationId": "listClinicas",
        "tags": [
          "clinicas"
        ],
        "summary": "Lista las seis clínicas",
        "description": "Devuelve un objeto indexado por id de clínica. El id es el que se usa como `location` en el resto de endpoints.",
        "responses": {
          "200": {
            "description": "Mapa de clínicas por id",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "$ref": "#/components/schemas/Clinica"
                  }
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/fisios": {
      "get": {
        "operationId": "listFisioterapeutas",
        "tags": [
          "fisioterapeutas"
        ],
        "summary": "Lista los fisioterapeutas visibles",
        "description": "Solo profesionales publicados. Incluye número de colegiado, que es dato público de un sanitario colegiado.",
        "parameters": [
          {
            "name": "location",
            "in": "query",
            "required": false,
            "description": "Filtra por id de clínica (ver /api/clinicas).",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Filtra por idioma que habla el profesional: nombre o código. Ej: ingles, en.",
            "schema": {
              "type": "string",
              "pattern": "^[a-zA-Z\\- ]{2,20}$"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Usa `marketplace_v1` para el contrato minimo y versionado de marketplace. Si se omite, la respuesta general no cambia.",
            "schema": {
              "type": "string",
              "enum": [
                "marketplace_v1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Listado de fisioterapeutas",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "type": "object",
                      "required": [
                        "count",
                        "fisioterapeutas"
                      ],
                      "properties": {
                        "count": {
                          "type": "integer"
                        },
                        "fisioterapeutas": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Fisioterapeuta"
                          }
                        }
                      }
                    },
                    {
                      "$ref": "#/components/schemas/MarketplaceProfessionalsV1"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "503": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/apps/available_from": {
      "get": {
        "operationId": "listHuecosEnRango",
        "tags": [
          "disponibilidad"
        ],
        "summary": "Huecos libres entre dos fechas",
        "description": "Devuelve los huecos libres agrupados por día. Es el endpoint recomendado para un agente: acota el rango y devuelve el formato reducido.\n\nSi no se pasan `from` y `to`, usa hoy y los siete días siguientes.",
        "parameters": [
          {
            "name": "location",
            "in": "query",
            "required": true,
            "description": "Id de clínica, o su nombre (ej: Getafe).",
            "schema": {
              "oneOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            }
          },
          {
            "name": "service",
            "in": "query",
            "required": true,
            "description": "Id de servicio. Ej: 545 = Sesión de Fisioterapia, 2753 = Masaje Descontracturante.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Huecos agrupados por fecha (clave YYYY-MM-DD)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/Hueco"
                    }
                  }
                },
                "examples": {
                  "getafe": {
                    "summary": "Getafe, sesión de fisioterapia",
                    "value": {
                      "2026-08-22": [
                        {
                          "worker": 47802,
                          "worker_name": "Piero Seminario",
                          "location": 1,
                          "start": "2026-08-22 12:00:00"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/apps/available": {
      "get": {
        "operationId": "listHuecos",
        "tags": [
          "disponibilidad"
        ],
        "summary": "Huecos libres (formato completo)",
        "description": "Como available_from pero sin acotar el rango y con más campos por hueco. Para un agente suele ser preferible available_from.",
        "parameters": [
          {
            "name": "location",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "service",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Huecos libres",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/servicios": {
      "get": {
        "operationId": "listServicios",
        "tags": [
          "servicios"
        ],
        "summary": "Catálogo de servicios con precios",
        "description": "Devuelve todos los servicios y bonos con su precio. **El campo `id` es el que se pasa como `service` a los endpoints de disponibilidad**, así que este es el punto de partida para reservar.\n\nIncluye `precio_por_sesion`, que en los bonos ya viene dividido.\n\nLas promociones se describen sin código: el descuento de bienvenida va en el enlace de la oferta de la portada, no hay cupón que publicar.",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Usa `marketplace_v1` para el contrato minimo y versionado de marketplace. Si se omite, la respuesta general no cambia.",
            "schema": {
              "type": "string",
              "enum": [
                "marketplace_v1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Catálogo",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "required": [
                        "count",
                        "servicios"
                      ],
                      "properties": {
                        "count": {
                          "type": "integer"
                        },
                        "moneda": {
                          "type": "string",
                          "examples": [
                            "EUR"
                          ]
                        },
                        "nota": {
                          "type": "string"
                        },
                        "promociones": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Promocion"
                          }
                        },
                        "servicios": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Servicio"
                          }
                        }
                      }
                    },
                    {
                      "$ref": "#/components/schemas/MarketplaceServicesV1"
                    }
                  ]
                }
              }
            }
          },
          "503": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/buscar": {
      "get": {
        "operationId": "buscarContenido",
        "tags": [
          "busqueda"
        ],
        "summary": "Buscar en el contenido del sitio",
        "description": "Busca en síntomas, patologías, lesiones, tratamientos y blog. Devuelve **solo URLs que responden 200**: el índice interno conserva entradas de consolidaciones antiguas cuyas URLs redirigen, y se descartan antes de responder.\n\n🔴 El parámetro es `buscar`, no `q`.",
        "parameters": [
          {
            "name": "buscar",
            "in": "query",
            "required": true,
            "description": "Término. Ej: fascitis.",
            "schema": {
              "type": "string",
              "maxLength": 80
            }
          },
          {
            "name": "limite",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 20,
              "default": 8
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resultados vivos",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "consulta": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "descartados": {
                      "type": "integer",
                      "description": "Resultados quitados por no responder 200."
                    },
                    "resultados": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "titulo": {
                            "type": "string"
                          },
                          "tipo": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri"
                          },
                          "tambien_llamado": {
                            "type": [
                              "array",
                              "null"
                            ],
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "503": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/huecos": {
      "get": {
        "operationId": "huecosTodasLasClinicas",
        "tags": [
          "disponibilidad"
        ],
        "summary": "Huecos libres en las seis clínicas a la vez",
        "description": "Responde «¿dónde puedo ir antes?» en una sola llamada, en vez de preguntar clínica por clínica. Ordena las clínicas por número de huecos y destaca el más próximo.",
        "parameters": [
          {
            "name": "servicio",
            "in": "query",
            "required": true,
            "description": "Id de servicio (ver /api/servicios).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "desde",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "hasta",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Huecos agregados",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "servicio_id": {
                      "type": "integer"
                    },
                    "total_huecos": {
                      "type": "integer"
                    },
                    "mas_proximo": {
                      "type": [
                        "object",
                        "null"
                      ]
                    },
                    "reservar_en": {
                      "type": "string",
                      "format": "uri"
                    },
                    "clinicas": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/especialistas": {
      "get": {
        "operationId": "recomendarEspecialistas",
        "tags": [
          "fisioterapeutas"
        ],
        "summary": "Recomienda fisioterapeutas por especialidad y cercanía",
        "description": "Responde a «fisio de suelo pélvico cerca de Chamberí» en una llamada. Sin parámetros devuelve el equipo. Es el motor de las herramientas MCP recomendar_especialista y fisio_cerca_de_mi.",
        "parameters": [
          {
            "name": "especialidad",
            "in": "query",
            "required": false,
            "description": "Lo que necesita la persona. Ej: suelo pélvico, ATM, deportiva, mandíbula. También en inglés: pelvic floor, sports, dry needling, TMJ, kids.",
            "schema": {
              "type": "string",
              "maxLength": 60
            }
          },
          {
            "name": "cerca_de",
            "in": "query",
            "required": false,
            "description": "Calle y número, barrio, estación de metro o lugar de Madrid o Getafe.",
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "lat",
            "in": "query",
            "required": false,
            "description": "Latitud. Va con lon y gana a cerca_de.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "lon",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "clinica",
            "in": "query",
            "required": false,
            "description": "Id de clínica (0-5). Gana a la ubicación.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "idioma",
            "in": "query",
            "required": false,
            "description": "Nombre o código. Ej: ingles.",
            "schema": {
              "type": "string",
              "maxLength": 20
            }
          },
          {
            "name": "canal",
            "in": "query",
            "required": false,
            "description": "Quién recomienda: va como utm_source en el enlace de reserva.",
            "schema": {
              "type": "string",
              "maxLength": 20
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "«en»: booking links in English (/en/appointments). Por defecto «es».",
            "schema": {
              "type": "string",
              "enum": [
                "es",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Especialistas por sede, de la más cercana a la más lejana. Si no hay ninguno, `especialistas` va vacío y `aviso` lista las especialidades que sí hay.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "aviso": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "ubicacion": {
                      "type": [
                        "object",
                        "null"
                      ]
                    },
                    "sedes": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "especialistas": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Especialista"
                      }
                    },
                    "reservar_en": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/primera-cita": {
      "get": {
        "operationId": "primeraCita",
        "tags": [
          "disponibilidad"
        ],
        "summary": "Primeros huecos libres con el especialista adecuado, cerca de la persona",
        "description": "Cada hueco trae `confirmar`: enlace a la confirmación con fisio, clínica, servicio y hora elegidos. No reserva.",
        "parameters": [
          {
            "name": "especialidad",
            "in": "query",
            "required": false,
            "description": "Lo que necesita. Ej: suelo pélvico, ATM. Sin ella, sesión de fisioterapia.",
            "schema": {
              "type": "string",
              "maxLength": 60
            }
          },
          {
            "name": "cerca_de",
            "in": "query",
            "required": false,
            "description": "Calle y número, barrio, metro o lugar de Madrid o Getafe.",
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "lat",
            "in": "query",
            "required": false,
            "description": "Latitud (va con lon).",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "lon",
            "in": "query",
            "required": false,
            "description": "Longitud.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "clinica",
            "in": "query",
            "required": false,
            "description": "Id de clínica 0-5.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "idioma",
            "in": "query",
            "required": false,
            "description": "Nombre o código. Ej: ingles.",
            "schema": {
              "type": "string",
              "maxLength": 20
            }
          },
          {
            "name": "canal",
            "in": "query",
            "required": false,
            "description": "Quién recomienda (utm_source).",
            "schema": {
              "type": "string",
              "maxLength": 40
            }
          },
          {
            "name": "limite",
            "in": "query",
            "required": false,
            "description": "1-10, por defecto 5.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "«en»: booking links in English (/en/appointments). Por defecto «es».",
            "schema": {
              "type": "string",
              "enum": [
                "es",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Huecos ordenados por fecha",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "aviso": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "ubicacion": {
                      "type": [
                        "object",
                        "null"
                      ]
                    },
                    "huecos": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "cuando": {
                            "type": "string"
                          },
                          "inicio": {
                            "type": "string"
                          },
                          "fisioterapeuta": {
                            "type": "string"
                          },
                          "clinica": {
                            "type": "string"
                          },
                          "direccion": {
                            "type": "string"
                          },
                          "distancia_km": {
                            "type": [
                              "number",
                              "null"
                            ]
                          },
                          "confirmar": {
                            "type": "string",
                            "format": "uri"
                          }
                        }
                      }
                    },
                    "reservar_en": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/Error"
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/reservar-hueco": {
      "get": {
        "operationId": "reservarHueco",
        "tags": [
          "disponibilidad"
        ],
        "summary": "Comprueba que un hueco sigue libre y devuelve el enlace a su confirmación",
        "description": "No reserva: la persona se identifica y confirma en eFISIO. Si ya no está libre, devuelve los huecos de ese día.",
        "parameters": [
          {
            "name": "clinica",
            "in": "query",
            "required": true,
            "description": "Id de clínica 0-5.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "inicio",
            "in": "query",
            "required": true,
            "description": "«YYYY-MM-DD HH:MM».",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "servicio",
            "in": "query",
            "required": false,
            "description": "Id de servicio. Por defecto 545.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "fisio",
            "in": "query",
            "required": false,
            "description": "Id del fisioterapeuta.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "canal",
            "in": "query",
            "required": false,
            "description": "utm_source.",
            "schema": {
              "type": "string",
              "maxLength": 40
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "«en»: booking links in English (/en/appointments). Por defecto «es».",
            "schema": {
              "type": "string",
              "enum": [
                "es",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "libre=true con `confirmar`, o libre=false con `alternativas`"
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/orientar": {
      "get": {
        "operationId": "orientar",
        "tags": [
          "busqueda"
        ],
        "summary": "De lo que cuenta la persona a nuestras fichas y al siguiente paso",
        "description": "Orienta, no diagnostica. Devuelve fichas (síntoma, patología, servicio), especialidad sugerida solo si la descripción la nombra, siguiente paso y cuándo ir antes al médico.",
        "parameters": [
          {
            "name": "descripcion",
            "in": "query",
            "required": true,
            "description": "Lo que le pasa, con sus palabras.",
            "schema": {
              "type": "string",
              "maxLength": 300
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Orientación"
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/opiniones-reales": {
      "get": {
        "operationId": "opiniones",
        "tags": [
          "clinicas"
        ],
        "summary": "Nota oficial de Google de una clínica y reseñas recientes; o las reseñas que nombran a un fisioterapeuta",
        "description": "Clínica: nota_google y total_google son las que publica Google (sincronizadas desde GBP). Fisio: menciones, sin media (no hay nota oficial por profesional). Los textos son los más recientes de 4 y 5 estrellas. Autor como «Nombre I.».",
        "parameters": [
          {
            "name": "clinica",
            "in": "query",
            "required": false,
            "description": "Id de clínica 0-5.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "fisio",
            "in": "query",
            "required": false,
            "description": "Id del fisioterapeuta. Gana a clinica.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limite",
            "in": "query",
            "required": false,
            "description": "1-10.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Opiniones"
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/fisioterapia-barata": {
      "get": {
        "operationId": "fisioterapiaBarata",
        "tags": [
          "servicios"
        ],
        "summary": "La forma más barata de empezar: primera sesión con la promoción de bienvenida y bonos",
        "description": "Precios del catálogo. Si la promoción de bienvenida está activa, la primera cita de un paciente nuevo se descuenta al pagar online (el enlace no lleva código). Con cerca_de o lat/lon añade la clínica más cercana.",
        "parameters": [
          {
            "name": "cerca_de",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 100
            }
          },
          {
            "name": "lat",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "lon",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "canal",
            "in": "query",
            "required": false,
            "description": "utm_source.",
            "schema": {
              "type": "string",
              "maxLength": 40
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "«en»: booking links in English (/en/appointments). Por defecto «es».",
            "schema": {
              "type": "string",
              "enum": [
                "es",
                "en"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Opciones de precio"
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/api/tarjeta-regalo": {
      "get": {
        "operationId": "tarjetaRegalo",
        "tags": [
          "servicios"
        ],
        "summary": "Tarjetas regalo: qué se puede regalar, desde cuánto y enlace para comprarla",
        "description": "Mismas categorías y precios «desde» que https://www.efisio.es/regalo.",
        "parameters": [
          {
            "name": "canal",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 40
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Tarjetas"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    }
  },
  "components": {
    "responses": {
      "Error": {
        "description": "Error estructurado",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "parametro": {
                "value": {
                  "error": {
                    "code": "invalid_location",
                    "message": "El parametro location debe ser un id numerico de clinica.",
                    "hint": "Consulta los ids en https://www.efisio.es/api/clinicas",
                    "docs": "https://www.efisio.es/openapi.json"
                  }
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Código estable, apto para ramificar."
              },
              "message": {
                "type": "string"
              },
              "hint": {
                "type": "string",
                "description": "Cómo corregirlo."
              },
              "docs": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "Clinica": {
        "type": "object",
        "properties": {
          "ID": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "description": "Nombre interno. Ej: Manuel Becerra."
          },
          "fullname": {
            "type": "string",
            "description": "Nombre público. Ej: Clínica Barrio Salamanca."
          },
          "salas": {
            "type": "integer"
          },
          "address": {
            "type": "string"
          },
          "address1": {
            "type": "string",
            "description": "Referencias de la zona."
          },
          "address2": {
            "type": "string",
            "description": "Código postal y ciudad."
          },
          "idsanitario": {
            "type": "string",
            "description": "Registro sanitario de la Comunidad de Madrid."
          }
        }
      },
      "Fisioterapeuta": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "fullname": {
            "type": "string"
          },
          "role": {
            "type": "string"
          },
          "colegiado": {
            "type": [
              "string",
              "null"
            ],
            "description": "Número de colegiado."
          },
          "location_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "location_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "languages": {
            "type": [
              "string",
              "null"
            ],
            "description": "Códigos ISO 639-1 separados por comas. Ej: es,en."
          },
          "especialidades": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Especialidades declaradas. Desde 1.1.0 es una lista (antes, texto JSON)."
          },
          "intro": {
            "type": [
              "string",
              "null"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "linkedin_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "imagen": {
            "type": "string",
            "description": "URL de la foto del fisioterapeuta; vacía si no tiene."
          },
          "avatar_recorte": {
            "type": "string",
            "description": "URL de la foto recortada sin fondo (WebP 400x480 con transparencia); vacía si aún no la hay."
          },
          "frase": {
            "type": "string",
            "description": "Frase corta del fisioterapeuta, la misma que muestra la web bajo su nombre; vacía si no la hay."
          },
          "iniciales": {
            "type": "string",
            "description": "Iniciales para el avatar cuando no hay foto."
          }
        }
      },
      "MarketplaceProfessionalsV1": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "count",
          "fisioterapeutas"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "minimum": 0
          },
          "fisioterapeutas": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MarketplaceProfessionalV1"
            }
          }
        }
      },
      "MarketplaceProfessionalV1": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "fullname",
          "colegiado",
          "intro",
          "location",
          "services"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "minimum": 1
          },
          "fullname": {
            "type": "string",
            "minLength": 1
          },
          "colegiado": {
            "type": "string",
            "minLength": 1
          },
          "intro": {
            "type": "string"
          },
          "location": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "integer",
                "minimum": 0
              }
            }
          },
          "services": {
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 1
            },
            "uniqueItems": true
          }
        }
      },
      "MarketplaceServicesV1": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MarketplaceServiceV1"
            }
          }
        }
      },
      "MarketplaceServiceV1": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "name",
          "duration",
          "price_range"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "minimum": 1
          },
          "name": {
            "type": "string",
            "minLength": 1
          },
          "duration": {
            "type": "integer",
            "minimum": 10,
            "maximum": 240
          },
          "price_range": {
            "type": "string"
          }
        }
      },
      "Hueco": {
        "type": "object",
        "properties": {
          "worker": {
            "type": "integer",
            "description": "Id del fisioterapeuta (ver /api/fisios)."
          },
          "worker_name": {
            "type": "string"
          },
          "location": {
            "type": "integer"
          },
          "start": {
            "type": "string",
            "description": "Inicio del hueco, hora de Madrid. Formato YYYY-MM-DD HH:MM:SS."
          }
        }
      },
      "Servicio": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Se pasa como `service` a /api/apps/available_from."
          },
          "nombre": {
            "type": "string"
          },
          "tipo": {
            "type": "string",
            "enum": [
              "sesion",
              "bono",
              "pack"
            ]
          },
          "sesiones": {
            "type": "integer"
          },
          "precio_eur": {
            "type": "number"
          },
          "precio_por_sesion": {
            "type": "number"
          },
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Promocion": {
        "type": "object",
        "properties": {
          "nombre": {
            "type": "string"
          },
          "descuento_eur": {
            "type": "number"
          },
          "aplica_a": {
            "type": "string"
          },
          "como": {
            "type": "string",
            "description": "Sin código: se asigna al reservar."
          }
        }
      },
      "Especialista": {
        "type": "object",
        "properties": {
          "nombre": {
            "type": "string"
          },
          "colegiado": {
            "type": "string"
          },
          "clinica_id": {
            "type": "integer"
          },
          "clinica": {
            "type": "string"
          },
          "direccion": {
            "type": "string"
          },
          "distancia_km": {
            "type": [
              "number",
              "null"
            ]
          },
          "especialidades": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Especialidades declaradas y servicios que ofrece en esa clínica."
          },
          "idiomas": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentacion": {
            "type": "string"
          },
          "perfil": {
            "type": "string",
            "format": "uri"
          },
          "reservar": {
            "type": "string",
            "format": "uri",
            "description": "Agenda con este fisioterapeuta ya elegido."
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "Servidor MCP y guía para agentes",
    "url": "https://www.efisio.es/llms.txt"
  }
}
