Rateshopper · API v0.1 · documentation
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.
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.
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.
# .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.
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.
| Paramètre | Défaut | Rôle |
|---|---|---|
| url | requis | 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. |
| checkin | requis | 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. |
| nights | 1 | Nombre de nuits, 1 à 30. |
| adults | 2 | 1 à 12. |
| children | 0 | 0 à 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. |
| rooms | 1 | 1 à 8. |
| currency | EUR | Devise demandée au moteur. Tous ne l'honorent pas : offers[].currency fait foi. |
| lang | fr | Influe sur les libellés rendus. |
| noCache | false | 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. |
| Champ | Sens |
|---|---|
| amount / currency | Prix total du séjour — sauf si basis dit autrement. |
| perNight | amount / nights, arrondi au centime. |
| basis | Absent 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. |
| board | Pension normalisée : RO, BB, HB, FB, AI. |
| refundable | true/false quand le moteur le dit, absent sinon. |
| touristTax | Taxe de séjour, quand elle est exposée. Réglée sur place, hors tarif. |
| roomsLeft | Chambres restantes, quand le moteur l'expose. |
metacached 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.
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.
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ètre | Défaut | Rôle |
|---|---|---|
| url | requis | Comme /rates. |
| from | requis | Première date d'arrivée. |
| to | — | Dernière date d'arrivée, incluse. Alternative à days. |
| days | 30 | Nombre de dates d'arrivée. Plafond CALENDAR_MAX_DAYS (92). |
| nights | 1 | Durée évaluée à chaque date d'arrivée. |
| anchors | 0 | 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 montantsCe 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 :
| source | Ce que le montant vaut |
|---|---|
| engine-search | Le moteur a réellement tarifé un séjour à cette date, restrictions et
packages appliqués. Vaut une réponse de /rates. |
| engine-calendar | La 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é. |
| inventory | L'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-restrictions | Aucun 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.
« Mes cinq concurrents sur trente jours » en un appel. Cache, limiteur par domaine et pool de navigateurs sont mutualisés sur toute la rafale.
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.
Découverte et état. Les trois dernières sont ouvertes, sans clef.
| Route | Rôle | Clef |
|---|---|---|
| GET /engines | Moteurs 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.json | Descripteur OpenAPI 3.1 complet, à donner tel quel à un générateur de client ou à un LLM outillé. | non |
| GET /llms.txt | Le mode d'emploi en texte brut, taillé pour tenir dans un contexte de modèle. | non |
| GET / | Banc de test interactif. | non |
| GET /moteurs | Relevé de marché : quels moteurs équipent l'hôtellerie française. | non |
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.
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.
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.
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.
Toute erreur rend { "error": { "code", "message", "details"? } }. Le code dit quoi refaire.
| Code | HTTP | Quoi faire |
|---|---|---|
| INVALID_QUERY | 400 | Corriger la requête. details.issues nomme le champ fautif. |
| UNAUTHORIZED | 401 | Clef absente, invalide ou révoquée. Ne pas réessayer sans en changer. |
| UNSUPPORTED_ENGINE | 422 | Aucune empreinte ne reconnaît cette URL. Essayer l'URL du bouton « Réserver », ou la fiche Booking.com. |
| BATCH_TOO_LARGE | 422 | Découper la rafale. |
| ENGINE_NOT_IMPLEMENTED | 501 | Ce moteur ne publie pas cette surface (typiquement un calendrier). /engines le disait. |
| UPSTREAM_ERROR | 502 | La source a mal répondu. Réessayer plus tard, pas en boucle. |
| BLOCKED | 502 | La source nous refuse (403/429). Lever le pied : le service ne contourne rien. |
| PARSE_ERROR | 502 | La source a changé de forme. C'est un bug à signaler, pas une donnée à interpréter. |
| BROWSER_DISABLED | 503 | Ce moteur exige un navigateur, désactivé sur cette instance. |
| UPSTREAM_TIMEOUT | 504 | La source n'a pas répondu dans le budget de temps. Réessayer plus tard. |
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.
{
"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.
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.
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.
| Moteur | Empreinte | Mode | Ce qu'il rend |
|---|---|---|---|
| thais | {instance}.thais-hotel.com/direct-booking | http | Total du séjour |
| mews | app.mews.com/distributor/{guid} | http | Total du séjour |
| dedge | secure-hotel-booking.com/…/{CODE}/… | http | Prix d'appel par nuit (basis: night) |
| misterbooking | …/module_booking_engine/index.php?id_etab={md5} | http | Prix vitrine |
| reservit | /fo/booking/{custid}/{hotelid}/, reserhotel.php | browser | Inventaire détaillé |
| ihotelier | reservations.travelclick.com/{id} | http | Total du séjour, sans nom d'hôtel |
| mygroomservice | neo-direct-booking.com/{CODE} | http | Total du séjour, occupation comprise |
| louvre | {ville}.{campanile|kyriad|premiereclasse|goldentulip}.com | http | Total du séjour, taxe et inventaire compris |
| siteminder | direct-book.com/[{groupe}/]properties/{slug} | http | Total du séjour |
| roomcloud | booking.roomcloud.net/be/se2/hotel.jsp?hotel={id} | http | Total du séjour, en une requête |
| booking | booking.com/hotel/{cc}/{slug}.html | http | Prix OTA, et repli des autres moteurs |