Rateshopper · API v0.1 · documentation

Lire les tarifs d'un hôtel à partir de l'URL de son moteur

On donne une URL de moteur de réservation et des dates ; le service identifie le moteur, l'interroge en temps réel et rend des offres normalisées. Pas d'interface applicative, pas de stockage de prix : une API de lecture, protégée par une clef.

Démarrer

Un appel, une URL d'hôtel, deux dates. Le moteur est déduit de l'URL — rien à déclarer au préalable.

# Le tarif d'un séjour précis
curl -s -H 'X-API-Key: VOTRE_CLEF' \
  'https://rateshopper.vadech.tech/rates?url=https://app.mews.com/distributor/f86e362a-8668-47dd-8e70-ac0000b6b1bd&checkin=2026-09-10&checkout=2026-09-12&adults=2' | jq
{
  "hotel":  { "engine": "mews", "hotelCode": "f86e…", "name": "MAH Hotel", "sourceUrl": "…" },
  "query":  { "checkin": "2026-09-10", "checkout": "2026-09-12", "nights": 2, "adults": 2, … },
  "available": true,
  "cheapest":  { "amount": 245.74, "currency": "EUR", "perNight": 122.87 },
  "offers": [ { "roomName": "Mah Somni XL - twin", "rateName": "Non remboursable",
                "board": "RO", "refundable": false, "amount": 245.74,
                "currency": "EUR", "perNight": 122.87, "roomsLeft": 7 } ],
  "meta": { "cached": false, "stale": false, "mode": "http", "durationMs": 3140,
            "fallbackUsed": false, "warnings": [] }
}

Le banc de test interactif est servi sur la racine : coller une URL, choisir deux dates, lire le résultat. Il utilise la même API que vous.

Authentification

Une clef partagée, fournie par l'exploitant du service. Elle nomme l'appelant et se révoque en éditant une variable d'environnement — il n'y a ni comptes ni jetons à rafraîchir.

# Les deux formes sont équivalentes
Authorization: Bearer VOTRE_CLEF
X-API-Key: VOTRE_CLEF

Sans clef valide, toute route de données répond 401 :

{ "error": { "code": "UNAUTHORIZED",
            "message": "Clef d API absente : ajoutez un en-tête `Authorization: Bearer <clef>`…" } }

Restent ouvertes, sans clef : / (banc de test), /docs, /moteurs, /health, /engines, /openapi.json et /llms.txt. Lire le contrat ne coûte aucun appel chez l'hôtelier et ne révèle aucun tarif : c'est ce qui permet à un agent de savoir quoi demander avant même d'avoir une clef.

Pas de clef en paramètre d'URL. Une query string finit dans les journaux d'accès, les historiques de navigateur et les traces de proxy. En-tête seulement.

Déclarer les clefs, côté exploitant

# .env — liste séparée par des virgules, chaque entrée `nom:clef` ou `clef`
API_KEYS=llm:$(openssl rand -hex 24),yield:$(openssl rand -hex 24)

Le nom sert uniquement à savoir qui appelle dans les journaux ; l'appelant, lui, ne présente que la clef. Révoquer, c'est retirer l'entrée et redémarrer. Les clefs ne sont conservées en mémoire que sous forme d'empreinte SHA-256, et la comparaison se fait à temps constant.

API_KEYS vide ⇒ service ouvert. C'est le mode développement local, et le seul où le service répond sans clef. Le démarrage l'écrit alors en warn dans les journaux. Toute exposition hors de la machine doit renseigner la variable.

Le séjour exact

La route qui fait foi dès qu'il s'agit d'annoncer un prix : c'est le moteur qui tarife, restrictions et packages appliqués.

GET/rates Toutes les offres (chambre × tarif) d'un hôtel pour une période donnée.
ParamètreDéfautRôle
urlrequis URL publique du moteur de réservation de l'hôtel, telle qu'elle figure derrière son bouton « Réserver ». /resolve dit à l'avance ce qu'elle vaut.
checkinrequis Date d'arrivée, YYYY-MM-DD, dans le fuseau de l'hôtel.
checkout— Date de départ. Alternative à nights ; fournir les deux exige qu'ils concordent.
nights1Nombre de nuits, 1 à 30.
adults21 à 12.
children00 à 10.
childrenAges— Âges séparés par des virgules (3,9). Exigé par certains moteurs pour tarifer ; si fourni, le nombre d'âges doit égaler children.
rooms11 à 8.
currencyEUR Devise demandée au moteur. Tous ne l'honorent pas : offers[].currency fait foi.
langfrInflue sur les libellés rendus.
noCachefalse true force une lecture amont. À réserver aux cas où la fraîcheur prime : c'est lent, et c'est une requête de plus chez l'hôtelier.

Ce que porte une offre

ChampSens
amount / currencyPrix total du séjour — sauf si basis dit autrement.
perNightamount / nights, arrondi au centime.
basisAbsent ou "stay" : le total du séjour. "night" : le moteur n'a publié qu'un prix d'appel par nuit — à ne pas multiplier, voir les règles.
boardPension normalisée : RO, BB, HB, FB, AI.
refundabletrue/false quand le moteur le dit, absent sinon.
touristTaxTaxe de séjour, quand elle est exposée. Réglée sur place, hors tarif.
roomsLeftChambres restantes, quand le moteur l'expose.

Ce que porte meta

cached et stale disent d'où vient la réponse (cache 15 min, puis péremption servie pendant qu'un rafraîchissement tourne). mode vaut http ou browser. fallbackUsed: true signale que le moteur natif a échoué et que Booking.com a pris le relais : c'est alors un prix OTA, pas le tarif direct, et cela doit être dit à l'utilisateur. warnings porte le reste — dates hors fenêtre de réservation, occupation ramenée à ce que le moteur accepte.

La grille par date d'arrivée

Un appel couvre des dizaines de dates là où /rates en couvre une. C'est la route pour observer la déformation des prix dans le temps — pas pour annoncer un montant.

GET/calendar Meilleur prix par date d'arrivée, sur une plage.
curl -s -H 'X-API-Key: VOTRE_CLEF' \
  'https://rateshopper.vadech.tech/calendar?url=<url_moteur>&from=2026-09-01&days=30&nights=1&anchors=3' | jq .support
ParamètreDéfautRôle
urlrequisComme /rates.
fromrequisPremière date d'arrivée.
to—Dernière date d'arrivée, incluse. Alternative à days.
days30Nombre de dates d'arrivée. Plafond CALENDAR_MAX_DAYS (92).
nights1Durée évaluée à chaque date d'arrivée.
anchors0 Mesure l'écart entre la grille et /rates sur N dates de la plage. Chaque ancrage coûte un appel amont complet.
adults, children, childrenAges, rooms, currency, lang, noCache— Identiques à /rates.

support : à lire avant les montants

Ce que chaque moteur met dans sa grille varie beaucoup, et la réponse le dit avant les prix. grain vaut best (un montant par date) ou room-rate (le détail par couple chambre × tarif, dans days[].cells). source vaut :

sourceCe que le montant vaut
engine-searchLe moteur a réellement tarifé un séjour à cette date, restrictions et packages appliqués. Vaut une réponse de /rates.
engine-calendarLa grille que le moteur publie pour son propre calendrier : prix d'appel à l'occupation par défaut, mais c'est bien lui qui l'a calculé.
inventoryL'inventaire brut, avant packages et règles d'occupation. Biaisé vers le bas, pas seulement imprécis. Utilisable comme forme de la courbe, jamais comme prix vendable. Aucun moteur ne publie plus cette valeur ; elle reste au contrat.
engine-restrictionsAucun montant. Le moteur publie ce qui est vendable — fermetures, COA/COD, min et max stay — mais ne chiffre rien sur cette surface. Les cases n'ont pas d'amount : la grille dit où le prix existe, et anchors=N achète les montants réels date par date.

occupancyAware et losAware disent si la grille tient compte de l'occupation et de la durée demandées ; daysPerCall, combien de dates un seul appel amont couvre. GET /engines publie tout cela avant l'appel : calendar: null signifie que le moteur n'expose aucune grille, et /calendar répondra ENGINE_NOT_IMPLEMENTED plutôt que d'émettre un appel par date dans votre dos.

Les ancrages se publient, ils ne s'appliquent pas. anchors=N compare la grille au prix réellement facturé sur N dates et rend l'écart dans anchors[] (gap, gapPct). Le service ne corrige rien avec : recalibrer une grille, ce serait fabriquer des prix. L'écart est mesuré et publié, l'aval décide. Un écart n'est publié que si les deux côtés sont comparables : une grille qui n'accepte aucune devise rend celle de l'hôtel quand /rates convertit vers celle demandée, et l'écart ne serait alors qu'un taux de change — gap reste null, note dit pourquoi.

La rafale concurrentielle

« Mes cinq concurrents sur trente jours » en un appel. Cache, limiteur par domaine et pool de navigateurs sont mutualisés sur toute la rafale.

POST/rates/batch Produit cartésien urls × dates.
curl -s -X POST 'https://rateshopper.vadech.tech/rates/batch' \
  -H 'X-API-Key: VOTRE_CLEF' -H 'content-type: application/json' -d '{
    "urls": ["https://app.mews.com/distributor/<guid>", "https://demo.thais-hotel.com/direct-booking"],
    "dates": [{"checkin":"2026-09-10","checkout":"2026-09-12"}, {"checkin":"2026-10-01","nights":1}],
    "adults": 2 }' | jq .meta

Chaque élément de results porte son propre status (ok avec data, ou error avec {code, message}) : les résultats sont toujours partiels plutôt que globalement en échec — un concurrent injoignable ne fait pas tomber les autres. meta récapitule requested, cached, fetched, failed, durationMs.

Au-delà de BATCH_MAX_PAIRS couples (200 par défaut), l'appel entier est refusé en 422 BATCH_TOO_LARGE — découper côté appelant.

Routes de service

Découverte et état. Les trois dernières sont ouvertes, sans clef.

RouteRôleClef
GET /enginesMoteurs supportés, leur mode (http / browser) et ce que vaut leur calendrier. À appeler avant /calendar. Ouverte : sa liste est déjà dans le descripteur.non
GET /resolve?url=Quel moteur derrière cette URL, et quel code hôtel. Reconnaissance seule, aucun appel amont : gratuit et instantané. Utile pour valider une URL collée par un utilisateur.oui
GET /healthÉtat de Redis, du navigateur, requêtes en vol, et mode d'authentification.non
GET /openapi.jsonDescripteur OpenAPI 3.1 complet, à donner tel quel à un générateur de client ou à un LLM outillé.non
GET /llms.txtLe mode d'emploi en texte brut, taillé pour tenir dans un contexte de modèle.non
GET /Banc de test interactif.non
GET /moteursRelevé de marché : quels moteurs équipent l'hôtellerie française.non

Trois règles à ne pas enfreindre

Elles ne se déduisent d'aucun schéma, et les trois produisent un prix faux ou un faux diagnostic. Un prix faux est pire qu'une absence de prix.

01

Complet n'est pas une erreur

Un hôtel sans disponibilité rend HTTP 200, available: false, offers: []. Une source injoignable rend un code d'erreur typé (502/504). Annoncer « complet » quand la source était simplement injoignable est une faute : tout le yield en aval s'appuie sur cette distinction.

02

Ne jamais multiplier un basis: "night"

Ce moteur-là (D-EDGE) ne publie qu'un meilleur prix par nuit. Le multiplier par le nombre de nuits écraserait les tarifs qui varient d'une nuit à l'autre et rendrait un total faux. Le service s'y refuse et transmet le montant tel quel, marqué. À l'appelant de tester le champ avant de sommer quoi que ce soit.

03

Ne jamais recomposer un prix depuis un inventaire

Une grille support.source: "inventory" est l'inventaire brut, avant packages et règles d'occupation : 120 € d'écart mesurés sur un cas réel. Même prudence pour une case days[].cells[] marquée packaged: true — son tarif comporte des articles liés que la source n'a pas chiffrés, le montant est donc inférieur au prix vendable. Pour un prix vendable, c'est /rates, qui interroge la recherche de séjour du moteur.

Codes d'erreur

Toute erreur rend { "error": { "code", "message", "details"? } }. Le code dit quoi refaire.

CodeHTTPQuoi faire
INVALID_QUERY400Corriger la requête. details.issues nomme le champ fautif.
UNAUTHORIZED401Clef absente, invalide ou révoquée. Ne pas réessayer sans en changer.
UNSUPPORTED_ENGINE422Aucune empreinte ne reconnaît cette URL. Essayer l'URL du bouton « Réserver », ou la fiche Booking.com.
BATCH_TOO_LARGE422Découper la rafale.
ENGINE_NOT_IMPLEMENTED501Ce moteur ne publie pas cette surface (typiquement un calendrier). /engines le disait.
UPSTREAM_ERROR502La source a mal répondu. Réessayer plus tard, pas en boucle.
BLOCKED502La source nous refuse (403/429). Lever le pied : le service ne contourne rien.
PARSE_ERROR502La source a changé de forme. C'est un bug à signaler, pas une donnée à interpréter.
BROWSER_DISABLED503Ce moteur exige un navigateur, désactivé sur cette instance.
UPSTREAM_TIMEOUT504La source n'a pas répondu dans le budget de temps. Réessayer plus tard.

Brancher un LLM

Deux surfaces, faites pour ça : /openapi.json décrit ce qui est appelable, /llms.txt dit comment s'en servir sans dire de bêtise. Les deux sont publiques et se lisent sans clef.

Définition d'outil, à donner au modèle

{
  "name": "rateshopper_get_rates",
  "description": "Tarifs et disponibilité d'un hôtel pour un séjour, à partir de l'URL de son moteur de réservation. available:false + offers:[] signifie complet, ce n'est pas une erreur. Ne jamais multiplier un montant basis:'night' par le nombre de nuits.",
  "input_schema": {
    "type": "object",
    "properties": {
      "url":      { "type": "string", "description": "URL du moteur de réservation de l'hôtel" },
      "checkin":  { "type": "string", "description": "YYYY-MM-DD" },
      "checkout": { "type": "string", "description": "YYYY-MM-DD, ou utiliser nights" },
      "nights":   { "type": "integer" },
      "adults":   { "type": "integer", "default": 2 }
    },
    "required": ["url", "checkin"]
  }
}

L'exécution de l'outil, côté serveur, est un simple GET /rates avec l'en-tête X-API-Key. La clef reste chez vous : elle ne doit jamais être placée dans le contexte du modèle ni demandée à l'utilisateur final.

Prompt système, en trois lignes

Tu disposes de rateshopper (/llms.txt pour le mode d'emploi complet).
- available:false + offers:[] = complet, HTTP 200. Une erreur 5xx = source injoignable. Ne pas confondre.
- basis:"night" = prix d'appel par nuit : ne pas multiplier, le dire tel quel.
- meta.fallbackUsed:true = prix OTA Booking.com, pas le tarif direct : le signaler.

Le contrat se lit à la source. /openapi.json et /llms.txt sont générés depuis le registre d'adapters : la liste des moteurs et la valeur de leurs calendriers y sont toujours celles de l'instance interrogée, jamais une copie qui vieillit.

Moteurs reconnus

L'empreinte porte sur le chemin et les paramètres, pas sur le domaine : plusieurs moteurs sont déployés en marque blanche sur le domaine de l'hôtelier. La liste vivante est sur GET /engines ; le relevé de marché complet est sur /moteurs.

MoteurEmpreinteModeCe qu'il rend
thais{instance}.thais-hotel.com/direct-bookinghttpTotal du séjour
mewsapp.mews.com/distributor/{guid}httpTotal du séjour
dedgesecure-hotel-booking.com/…/{CODE}/…httpPrix d'appel par nuit (basis: night)
misterbooking…/module_booking_engine/index.php?id_etab={md5}httpPrix vitrine
reservit/fo/booking/{custid}/{hotelid}/, reserhotel.phpbrowserInventaire détaillé
ihotelierreservations.travelclick.com/{id}httpTotal du séjour, sans nom d'hôtel
mygroomserviceneo-direct-booking.com/{CODE}httpTotal du séjour, occupation comprise
louvre{ville}.{campanile|kyriad|premiereclasse|goldentulip}.comhttpTotal du séjour, taxe et inventaire compris
siteminderdirect-book.com/[{groupe}/]properties/{slug}httpTotal du séjour
roomcloudbooking.roomcloud.net/be/se2/hotel.jsp?hotel={id}httpTotal du séjour, en une requête
bookingbooking.com/hotel/{cc}/{slug}.htmlhttpPrix OTA, et repli des autres moteurs