Recetas/Pon un panel de reputación en tu propia web
Código Feed20 min

Pon un panel de reputación en tu propia web

La API devuelve puntuaciones, recuentos, notas por categoría y texto. No devuelve sentimiento, y no devuelve "la limpieza sube". Esta receta construye las dos cosas — y las dos guardas que impiden que una muestra de cinco reseñas haga crecer una flecha muy segura de sí misma.

Pon un panel de reputación en tu propia web

Qué vas a construir

Un panel de reputación en un shadow root: una nota ponderada entre todas las OTAs, una barra de sentimiento, tarjetas de categoría que solo enseñan tendencia cuando la muestra la sostiene, y las reseñas con las respuestas del hotel — en claro y oscuro, en español e inglés.

Esto hay que montarlo antes

Obligatorio

Los endpoints no aceptan texto libre: aceptan tus identificadores. Esta receta da por hecho que lo de abajo ya existe en tu cuenta, y mientras falte algo la llamada responde vacía, no da error. Cada punto enlaza la receta que lo enseña.

  1. 01

    Da de alta el hotel en tu panel de control

    Veetal resuelve el alojamiento contra Booking.com y le asigna un slug — y es ese slug, no el nombre del hotel, lo que piden todos los endpoints de alojamiento.

    Guía: Da de alta tu hotel y el compset contra el que fijas precio →
  2. 02

    Lanza una importación sobre el hotel

    Un dataset Feed solo contiene lo que una importación ha escrito. Activa esta API sobre el hotel, lanza la primera ejecución a mano y lee la estimación de créditos antes de pulsar: el coste crece con el compset y con las OTAs.

    Guía: Importar la reputación de un hotel de Booking →

Lo mismo por API, si prefieres no pasar por el panel:

cURL
curl -X POST "https://api.veetal.app/v2/account/accommodation" \
  -H "veetal-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ ... }'   # see the account reference for the body

Para saber si ya ha terminado alguna importación, lístalas:

cURL
curl "https://api.veetal.app/v2/account/imports" \
  -H "veetal-api-key: YOUR_API_KEY"

Las lecturas Feed no se facturan por petición. Los créditos se van en las importaciones que recogen los datos, y por eso la estimación aparece antes de lanzar y no después.

Cómo funciona

  1. 01Prepara el hotel
  2. 02Pide todas las OTAs de una vez
  3. 03Lo que te da la API y lo que tienes que calcular tú
  4. 04El proxy
  5. 05El panel
  6. 06Empótralo
  7. 07Si algo sale mal
  8. 08Qué viene después

Paso a paso

Al terminar tendrás un panel de reputación en tu propia web: una nota construida con todas las OTAs en las que está tu hotel, el reparto de sentimiento que hay detrás, qué categorías se están moviendo y las reseñas — todo tras un proxy que mantiene tu API key en privado.

Qué necesitas: la API Feed · Reputation instalada y activa, el hotel dado de alta y un import terminado.

Lo que cuesta: nada por visita. Las lecturas Feed no se facturan por petición — los créditos se van en los imports que recogen los datos.

1. Prepara el hotel

Da de alta el hotel en Accommodations y mira Profiles Found: las OTAs que se detecten ahí son las que este widget puede mezclar. Un hotel con Booking, Google y Tripadvisor da un panel mucho mejor que uno solo con Google, y leerlo cuesta lo mismo.

Después ve a Reputation → Accommodations y lanza un import con todas las OTAs que quieras en la media seleccionadas, no solo una. Pon un schedule ya que estás; con una ejecución diaria el widget nunca enseña reseñas de más de 24 horas. El icono de copiar junto al hotel te da el slug con el que lo identifica la API.

2. Pide todas las OTAs de una vez

El error que conviene evitar pronto: provider es opcional tanto en reputation como en reviews. Si filtras por él te llevas una OTA. Si lo omites te las llevas todas en una sola llamada — accommodation vuelve como lista, con una entrada por OTA.

CODE
GET /v2/feed/accommodation/{slug}/reputation
GET /v2/feed/accommodation/{slug}/reviews?include_competitors=false&limit=200

Dos cosas sobre esa segunda línea:

  • include_competitors viene a true por defecto. Si lo olvidas, tu propia web empieza a enseñar las reseñas de tu competencia.
  • limit topa en 200. Pide 500 y recibes un 400 con el código 226. Dos ventanas de 28 días caben de sobra en las 200 reseñas más recientes.

Pruébalo en el Playground antes de escribir código. En un hotel real, la llamada de reputación respondió tres fuentes a la vez:

JSON
[
  { "provider": "booking",     "review_score": 8.5, "review_count": 2468 },
  { "provider": "google",      "review_score": 9.0, "review_count": 1691 },
  { "provider": "tripadvisor", "review_score": 8.8, "review_count": 437 }
]

3. Lo que te da la API y lo que tienes que calcular tú

Esta es la parte que decide si el panel merece la pena. La API devuelve puntuaciones, recuentos, notas por categoría y texto. No devuelve un campo de sentimiento, y no devuelve "la limpieza sube un 12 %".

Así que el widget deriva cuatro cosas, y guarda todos los umbrales en un solo fichero (insights.mjs) para que las decisiones se vean en vez de quedar enterradas:

DerivadoCómoUmbral
Nota principalMedia de todas las OTAs, ponderada por número de reseñas
SentimientoTramos sobre la puntuación de 0 a 10positiva ≥ 9 · neutra 7-8,9 · negativa < 7
Nota por categoríaMedia por categoría en la ventana28 días
TendenciaVariación contra los 28 días anteriores≥ 8 muestras a cada lado, ≥ 1 % de movimiento

Pondera la nota principal. En el hotel de arriba, la media simple de 8,5, 9,0 y 8,8 es 8,77. Ponderada por número de reseñas es 8,71 — Booking tiene 2.468 reseñas y merece tirar más que las 437 de Tripadvisor. Una media sin ponderar deja que una OTA con nueve reseñas grite tan alto como una con dos mil.

Protege las tendencias o te mentirán. Los dos umbrales de esa tabla salieron de datos reales, y los dos se añadieron después de ver al widget decir algo falso:

  • cleanliness tenía 5 puntuaciones en la ventana actual contra 2 en la anterior. Da para escribir un "−4 %" y no da ni de lejos para sostenerlo. Un huésped tuvo una mala mañana.
  • location tenía unas sanas 47 contra 23 muestras y se movió un −0,3 %. Redondeado, la tarjeta pintaba un solemne "↓0 %" — una flecha apuntando a nada, que un hotelero lee como un problema donde no lo hay.

Una categoría que no pasa alguna de las dos guardas sigue enseñando su nota. Simplemente no le crece una flecha.

Mezclar es lo que hace que las categorías valgan la pena. Los nombres llegan ya normalizados —location significa lo mismo venga de Google o de Tripadvisor— pero cada OTA expone un subconjunto distinto. Tripadvisor devuelve cleanliness, value_for_money y sleep_quality; Google devuelve service, location y rooms. Mezcladas, te quedan siete categorías donde un widget de una sola OTA tiene tres.

4. El proxy

La Connect API acepta peticiones cross-origin —devuelve access-control-allow-origin: *— así que tu página podría llamarla directamente desde el navegador. No lo hagas. La petición lleva tu veetal-api-key, y cualquiera que abra las devtools se va con ella y puede leer toda tu cuenta.

La API key no sale nunca de tu servidor. El widget habla con tu dominio; tu dominio habla con Veetal.

JAVASCRIPT
export async function build() {
  const [reputation, reviews] = await Promise.all([
    // Sin filtro de provider: una entrada por OTA, que es lo que convierte esto
    // de un widget de Google en un widget de reputación.
    veetalOptional(`/feed/accommodation/${SLUG}/reputation`),
    veetal(`/feed/accommodation/${SLUG}/reviews?include_competitors=false&limit=200`),
  ]);

  const own = (reputation && reputation.accommodation) || [];
  const all = reviews.reviews || [];

  const sources = own.map((entry) => ({
    provider: entry.provider,
    score: entry.review_score ?? null,
    count: entry.review_count ?? null,
  }));

  return {
    name: (own[0] && own[0].accommodation_name) || null,
    window_days: WINDOW_DAYS,
    headline: headline(sources),   // ponderada entre OTAs
    sources,
    sentiment: sentiment(all),     // todas las reseñas con nota, tengan texto o no
    categories: categories(all).slice(0, 6),
    reviews: all
      .filter((r) => r.text && r.text.trim())  // el 27 % trae nota y ningún texto
      .slice(0, 12)
      .map(toCard),
  };
}

Fíjate en qué población alimenta a qué: el sentimiento y las tendencias usan todas las reseñas con nota, incluido ese 27 % sin texto. La lista visible usa lo contrario. La misma carga, dos preguntas distintas.

El endpoint que va encima cachea quince minutos y, si un refresco falla, sirve la última carga buena — una página que funcionaba hace un minuto no puede quedarse en blanco porque haya fallado una llamada.

5. El panel

El widget se renderiza en un Shadow DOM, así que la página anfitriona no puede colarse dentro ni él puede salirse. Todo se construye con textContent, nunca con innerHTML: son textos escritos por desconocidos pintados sobre una web comercial.

El widget con nota, sentimiento, tendencia por categoría y reseñas

La nota se lleva el espacio que merece, la línea de meta dice de qué está hecha —media, tres fuentes, 28 días— y la barra de sentimiento es una sola regla apilada en vez de una librería de gráficas. Debajo, las categorías: dos con movimiento real y cuatro enseñando su nota porque su muestra era demasiado fina para comparar. Service ↓3 % y Rooms ↓3 % son las dos únicas afirmaciones que los datos sostienen, y son las dos sobre las que un revenue manager debería actuar.

Sigue el esquema de color del sistema, y lo sigue también si el visitante lo cambia con la página abierta:

El mismo widget en modo oscuro

6. Empótralo

HTML
<div id="reviews"></div>

<script src="/veetal-reviews-widget.js"
        data-endpoint="/reviews-widget.json"
        data-target="#reviews"
        data-limit="5"
        data-locale="es"
        data-panel="full"
        data-theme="auto"></script>

data-panel decide cuánto se ve: full es nota + sentimiento + temas + reseñas, summary es solo el panel (va bien en una barra lateral), list son solo las reseñas. data-locale elige el idioma de las etiquetas —es o en— y formatea los números, así que la nota se lee 4,36 en español y 4.36 en inglés.

Si algo sale mal

Qué vesQué significaQué hacer
400 con código 226limit por encima de 200Pide 200 y pagina con ?page=2 si necesitas más
NoReviewsDataFound (785)No hay reseñas guardadas de ese hotelRevisa sus perfiles y que haya terminado un import
NoReputationDataFound (784)El último import no trajo reputaciónEs lo esperable tras una ejecución solo de reseñas. El panel degrada en vez de morirse
Una sola fuenteTe has dejado provider en la peticiónQuítalo — sin filtro entran todas las OTAs
Reseñas de la competencia en tu webinclude_competitors se quedó por defectoPonlo a false
Todas las categorías con flechaHas quitado las guardasDevuélvelas. Las muestras pequeñas producen tonterías muy seguras de sí mismas

Qué viene después

  • Añade schema.org/AggregateRating para que la nota combinada salga en los propios resultados de Google.
  • Sigue la nota principal en el tiempo llamando a /reputation con import_date y pinta un sparkline al lado.
  • Parte la barra de sentimiento por OTA — al mismo hotel se le suele leer muy distinto en Booking y en Tripadvisor, y ese hueco es en sí mismo un hallazgo.

Clona y ejecuta

Ejecutable

La receta entera es una carpeta autocontenida: Node 20, sin paso de build, y una batería de tests que corre sin API key.

Terminal
git clone https://github.com/Veetal-Connect/recipes.git
cd recipes/google-reviews-widget
cp .env.example .env          # put your VEETAL_API_KEY and slug in it
npm install
node --env-file=.env server.mjs   # http://localhost:8787
Ver el repositorio ↗ Stack: Node 20 · Express · vanilla JS · no build step

Comprueba antes tu key

Una llamada, cinco segundos. Si esto responde, la receta va a funcionar.

cURL
# todas las OTAs del hotel, una entrada por cada una
curl "https://api.veetal.app/v2/feed/accommodation/YOUR_ACCOMMODATION_SLUG/reputation" \
  -H "veetal-api-key: YOUR_API_KEY"

# las reseñas: sin provider entran todas, y limit topa en 200
curl "https://api.veetal.app/v2/feed/accommodation/YOUR_ACCOMMODATION_SLUG/reviews?include_competitors=false&limit=200" \
  -H "veetal-api-key: YOUR_API_KEY"

Preguntas

¿La API devuelve una nota de sentimiento?

No. Devuelve reseñas y reputación: puntuaciones, recuentos, notas por categoría y texto. El sentimiento lo deriva el widget repartiendo la puntuación de 0 a 10 en tramos —positiva desde 9, negativa por debajo de 7— y esos umbrales son una decisión de producto, no un número de Veetal. Viven en un solo fichero para que puedas cambiarlos.

¿Por qué mi nota principal es más baja que la media de mis OTAs?

Porque está ponderada por número de reseñas. En un hotel real la media simple de 8,5, 9,0 y 8,8 era 8,77 y la ponderada 8,71: Booking tenía 2.468 reseñas frente a las 437 de Tripadvisor, así que tira más. Una media sin ponderar deja que una OTA casi vacía grite tan alto como una llena.

¿Por qué unas categorías enseñan un porcentaje y otras solo un número?

Porque un porcentaje necesita evidencia suficiente. Una categoría solo recibe flecha cuando las dos ventanas de 28 días tienen al menos ocho puntuaciones y el movimiento es de al menos un 1 %. Por debajo de eso enseña solo su nota. Caso real: limpieza comparaba 5 reseñas contra 2, que da para escribir un "−4 %" y no da ni de lejos para sostenerlo.

¿Cómo consigo más de una OTA en el panel?

Deja `provider` fuera de la petición. Es opcional tanto en reputation como en reviews, y sin filtro entran todas las OTAs que cubriera el último import — `accommodation` vuelve como lista con una entrada por cada una. Después asegúrate de que tu import seleccionó de verdad esas OTAs en el dashboard.

La llamada de reseñas responde 400 con código 226

`limit` está topado en 200. Pide 200 y pagina con `?page=2` si necesitas más histórico. Dos ventanas de 28 días caben en las 200 reseñas más recientes de un hotel con tráfico normal.

¿Puedo enseñar solo el panel, sin la lista de reseñas?

Sí: `data-panel="summary"` pinta la nota, la barra de sentimiento y las categorías sin lista, que encaja en una barra lateral. `data-panel="list"` hace lo contrario. El valor por defecto, `full`, lo enseña todo.

Construye el tuyo

Empieza gratis con 100 créditos de API. Sin tarjeta ni llamada comercial.

Whatsapp