Rate shopping comparando lo comparable
La tarifa más barata de cada hotel es la comparación que monta todo el mundo primero, y falla sin llegar a parecer rota. En un compset real producía un gap del 28 % contra un competidor que no vende ninguna tarifa flexible. Esta es la rejilla que no lo hace.

Qué vas a construir
Un comando que imprime una rejilla producto × hotel para una noche, tu puesto en cada producto contando solo a los hoteles que lo venden, y un aviso explícito por cada celda vacía — más un CSV para la hoja de cálculo.
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
Rellena su compset
Hasta diez competidores en el detalle del alojamiento. Sin ellos esta receta imprime tu propia lista de precios en vez de una comparación, y el bloque de competidores vuelve vacío.
- 03
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
- 01El informe que todo el mundo monta primero, y por qué miente
- 02Pon las tarifas a correr
- 03Lee el endpoint correcto
- 04Define qué significa "comparable"
- 05Monta la rejilla, y deja los huecos abiertos
- 06Ejecútalo
- 07Qué viene después
Paso a paso
Al terminar tendrás un comando que responde a "dónde estoy de verdad frente a mi compset esta noche" — producto a producto, en vez de la tabla de tarifa-más-barata-por-hotel que compara en silencio una flexible contra una no reembolsable.
Qué necesitas: el hotel en tu cuenta con su compset relleno, el feed de Accommodation Rates activo y un import terminado.
Lo que cuesta: nada por ejecución. Las lecturas Feed no se facturan por petición — los créditos se van en los imports que recogen las tarifas.
El informe que todo el mundo monta primero, y por qué miente
Tarifa más barata de cada hotel, una al lado de otra. Es la comparación evidente y falla sin llegar a parecer rota nunca.
Esta es una noche real del feed, puesta como toca:
| Solo aloj. · Flex | Desayuno · Flex | Solo aloj. · NR | Desayuno · NR | |
|---|---|---|---|---|
| Tu hotel | 411,40 | 435,20 | — | — |
| Competidor A | — | — | 567,40 | 631,40 |
| Competidor B | 1.010,90 | 1.041,70 | 2.220,90 | 2.251,70 |
La tarifa más barata por hotel dice: 411,40 · 567,40 · 1.010,90. Estás un 28 % por debajo del Competidor A. Buena noticia, y falsa.
El Competidor A no vende ninguna tarifa flexible. Esos 567,40 son no reembolsables. Estás comparando tu tarifa cancelable en cualquier momento contra una que el huésped no puede cancelar, y llamando gap de precio a la diferencia. En el producto que ambos vendéis de verdad no hay nada que comparar — y eso es en sí mismo el hallazgo, el que nunca sobrevive a un informe de un solo número.
Dos cosas más que se ven en la rejilla y son invisibles en la lista:
- La no reembolsable del Competidor B cuesta más que su flexible — 2.220,90 contra 1.010,90. "La no reembolsable es la barata" es una suposición, no una regla, y aquí está del revés.
- Tú no vendes nada en la columna de no reembolsable. Sea deliberado o un descuido, es una decisión que tomar — y no puedes tomarla si el informe promedia el hueco hasta hacerlo desaparecer.
1. Pon las tarifas a correr
Da de alta el hotel en Accommodations y rellena su compset — hasta diez competidores. Sin ellos esta receta imprime una lista de precios, no una comparación.
Después lanza un import en Accommodation Rates y pon un schedule. El feed solo responde por las fechas que ya haya cubierto un import: pide una noche que nadie importó y recibes un 404 limpio.
2. Lee el endpoint correcto
GET /v2/feed/accommodation/{slug}/rate/v7/{fecha}
Usa v7. La ruta antigua /rate hace de proxy contra el backend legacy y devuelve otra forma — una trampa que conviene conocer antes de montar un parser alrededor de la equivocada.
Lo que vuelve es { report: { [slug]: [tarifa, ...] } }: tu hotel y todos los competidores en la misma carga, cada tarifa con un bloque _meta que lleva is_primary, accommodation_name, rate_date, visitor_type y device_type.
Una tarifa tiene esta pinta:
{
"room_type": "Habitación Doble Estándar",
"price_per_night": 411.4,
"price_total": 411.4,
"currency": "EUR",
"meal_plan": { "breakfast": false, "half_board": false, "full_board": false, "all_inclusive": false },
"cancellation": { "free_cancellation": true, "free_cancellation_days": null, "no_refundable": false },
"payment": { "no_prepayment": false },
"is_genius": false,
"available_rooms": 1,
"minimum_nights": 1,
"_meta": { "is_primary": true, "accommodation_name": "Tu Hotel", "rate_date": "2026-08-21" }
}
meal_plan y cancellation son los dos bloques que hacen posible toda la receta. El resto es detalle.
Existe además ?min_rates=true, que reduce cada hotel a su única tarifa más barata. Es cómodo, y es justo el atajo contra el que argumenta esta receta: es el parámetro que tira a la basura el mix de productos.
3. Define qué significa "comparable"
Dos tarifas son comparables cuando venden lo mismo. Eso es un régimen más una política de cancelación —BB/flex, RO/nr— y las reglas viven en un solo fichero para que se puedan discutir:
export function board(rate) {
const meal = rate.meal_plan || {};
if (meal.all_inclusive) return 'AI';
if (meal.full_board) return 'FB';
if (meal.half_board) return 'HB';
if (meal.breakfast) return 'BB';
return 'RO';
}
// free_cancellation es la que decide; no_refundable es solo el respaldo, porque
// una tarifa puede llegar sin ninguna de las dos banderas puesta.
export function cancellation(rate) {
const policy = rate.cancellation || {};
if (policy.free_cancellation) return 'flex';
return 'nr';
}
export const productKey = (rate) => `${board(rate)}/${cancellation(rate)}`;
El régimen se comprueba del más inclusivo al menos porque las banderas se apilan: una tarifa de todo incluido también trae breakfast: true, y mirar el desayuno primero la archivaría como BB.
4. Monta la rejilla, y deja los huecos abiertos
const matrix = buildMatrix(report); // producto × hotel
const standing = position(matrix); // en qué puesto quedas en cada producto
La única regla que importa: una celda ausente se queda en null. Nunca en cero, nunca promediada, nunca rellenada con el precio que ese hotel tiene en otro producto. Una celda vacía significa que el competidor no vende ese producto esa noche, y tratarla como un cero es por donde empieza a mentir un informe de compset.
El ranking sale de ahí — el denominador cuenta solo a los hoteles que venden el producto:
Desayuno · Cancelación gratis #1 de 2 · eres el más barato
Desayuno · No reembolsable no vendes este — 2 competidor(es) sí
Solo alojamiento · Flexible #1 de 2 · eres el más barato
Solo alojamiento · No reemb. no vendes este — 2 competidor(es) sí
"#1 de 2" es una afirmación más pequeña y más verdadera que "#1 de 3". El compset de tres hoteles solo tiene dos en esa carrera.
5. Ejecútalo
node --env-file=.env index.mjs 2026-08-21
node --env-file=.env index.mjs 2026-08-21 --csv > rates.csv
El CSV da una fila por hotel y producto, listo para una hoja de cálculo o un almacén — y sigue omitiendo las celdas vacías en vez de ponerlas a cero.
Qué viene después
- Recorre un mes con
/rate/v7/{año}/{mes}y observa cómo un competidor abre o cierra su inventario flexible según se acerca la fecha. Ese cambio de mix suele aparecer antes que el cambio de precio. - La misma carga trae
is_genius,available_roomsyminimum_nights: un competidor más barato solo en tarifa Genius, o solo con tres noches mínimo, no es realmente más barato. - Maridala con el feed de paridad. El rate shopping te dice lo que cobra el mercado; la paridad te dice si tus propios canales están de acuerdo entre ellos.
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/rate-shopping-like-for-like
cp .env.example .env # your VEETAL_API_KEY and VEETAL_ACCOMMODATION_SLUG
node --env-file=.env index.mjs YYYY-MM-DD # put the night you want to priceComprueba antes tu key
Una llamada, cinco segundos. Si esto responde, la receta va a funcionar.
curl "https://api.veetal.app/v2/feed/accommodation/YOUR_ACCOMMODATION_SLUG/rate/v7/2026-08-21" \
-H "veetal-api-key: YOUR_API_KEY"
# un mes entero:
curl "https://api.veetal.app/v2/feed/accommodation/YOUR_ACCOMMODATION_SLUG/rate/v7/2026/08" \
-H "veetal-api-key: YOUR_API_KEY"Preguntas
¿Por qué no comparar simplemente la tarifa más barata de cada hotel?
Porque la tarifa más barata de dos hoteles suele ser dos productos distintos. En un compset real, un competidor no vendía ninguna tarifa flexible, así que su más barata era no reembolsable — compararla con una flexible producía un "gap" del 28 % que no existía. Comparable significa mismo régimen y misma política de cancelación.
¿Qué significa una celda vacía?
Que ese competidor no vende ese producto esa noche. Es un hallazgo, no un hueco: se queda en null, nunca se promedia y nunca se sustituye por el precio que ese hotel tiene en otro producto. Tratar las celdas vacías como ceros es la forma más rápida de que un informe de compset empiece a mentir.
¿No es siempre más barata la no reembolsable que la flexible?
No, y los datos lo dicen. En la noche que documenta esta receta, la no reembolsable de un competidor estaba a 2.220,90 frente a los 1.010,90 de su flexible — más del doble. Saca la suposición de tu modelo y lee lo que devuelve el feed.
¿Uso /rate o /rate/v7?
v7. La ruta antigua /rate hace de proxy contra el backend legacy y devuelve otra forma, así que un parser construido para una se rompe con la otra. Todo en esta receta usa /rate/v7/{fecha}, y existe /rate/v7/{año}/{mes} para un mes entero.
El endpoint me devuelve 404 en la fecha que quiero
El feed solo responde por las fechas que ya haya cubierto un import. Mira en la pestaña Imports de la API de Accommodation Rates qué fechas tienes de verdad, y pon un schedule para que la ventana siga avanzando.
¿Por qué no sale mi compset en la respuesta?
El compset sale del propio alojamiento, no de la petición. Rellénalo en la ficha del hotel en el dashboard —hasta diez competidores— y lanza un import. Sin él, el report contiene un solo hotel y no hay nada que comparar.
Construye el tuyo
Empieza gratis con 100 créditos de API. Sin tarjeta ni llamada comercial.