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.

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
ObligatorioLos 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.
- 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 → - 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 -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 bodyPara saber si ya ha terminado alguna importación, lístalas:
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
- 01Prepara el hotel
- 02Pide todas las OTAs de una vez
- 03Lo que te da la API y lo que tienes que calcular tú
- 04El proxy
- 05El panel
- 06Empótralo
- 07Si algo sale mal
- 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.
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_competitorsviene atruepor defecto. Si lo olvidas, tu propia web empieza a enseñar las reseñas de tu competencia.limittopa en 200. Pide 500 y recibes un400con el código226. 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:
[
{ "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:
| Derivado | Cómo | Umbral |
|---|---|---|
| Nota principal | Media de todas las OTAs, ponderada por número de reseñas | — |
| Sentimiento | Tramos sobre la puntuación de 0 a 10 | positiva ≥ 9 · neutra 7-8,9 · negativa < 7 |
| Nota por categoría | Media por categoría en la ventana | 28 días |
| Tendencia | Variació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:
cleanlinesstení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.locationtení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.
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.

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:

6. Empótralo
<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é ves | Qué significa | Qué hacer |
|---|---|---|
400 con código 226 | limit por encima de 200 | Pide 200 y pagina con ?page=2 si necesitas más |
NoReviewsDataFound (785) | No hay reseñas guardadas de ese hotel | Revisa sus perfiles y que haya terminado un import |
NoReputationDataFound (784) | El último import no trajo reputación | Es lo esperable tras una ejecución solo de reseñas. El panel degrada en vez de morirse |
| Una sola fuente | Te has dejado provider en la petición | Quítalo — sin filtro entran todas las OTAs |
| Reseñas de la competencia en tu web | include_competitors se quedó por defecto | Ponlo a false |
| Todas las categorías con flecha | Has quitado las guardas | Devuélvelas. Las muestras pequeñas producen tonterías muy seguras de sí mismas |
Qué viene después
- Añade
schema.org/AggregateRatingpara que la nota combinada salga en los propios resultados de Google. - Sigue la nota principal en el tiempo llamando a
/reputationconimport_datey 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
EjecutableLa receta entera es una carpeta autocontenida: Node 20, sin paso de build, y una batería de tests que corre sin API key.
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:8787Comprueba antes tu key
Una llamada, cinco segundos. Si esto responde, la receta va a funcionar.
# 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.