{"openapi":"3.1.0","info":{"title":"Rateshopper","version":"0.1.0","summary":"Lecture temps reel des disponibilites et tarifs d'un hotel a partir de l'URL de son moteur de reservation.","description":"On donne une URL de moteur de reservation (ou, a defaut, une fiche Booking.com) et des\ndates ; le service identifie le moteur, interroge la source et rend les offres normalisees.\n\nTrois regles a connaitre avant d exploiter une reponse :\n\n1. **Complet n'est pas une erreur.** Un hotel sans disponibilite rend HTTP 200 avec\n   `available: false` et `offers: []`. Une source injoignable rend un code d erreur typé\n   (401/4xx/5xx). Ne jamais confondre les deux.\n2. **Ne jamais multiplier un montant `basis: \"night\"`** par le nombre de nuits : le moteur\n   n'a publie qu'un prix d appel par nuit, les nuits suivantes peuvent valoir tout autre\n   chose. `basis` absent ou `\"stay\"` : le montant couvre le sejour entier.\n3. **`/calendar` et `/rates` ne mesurent pas la meme chose.** `/rates` tarife un sejour exact ;\n   `/calendar` rend une grille par date d arrivee dont la valeur depend de `support.source`\n   (`inventory` = inventaire brut, biaise vers le bas, jamais un prix vendable).","contact":{"name":"Thais Soft","email":"tech@thais-soft.com"}},"servers":[{"url":"https://rateshopper.vadech.tech"}],"security":[{"ApiKeyHeader":[]},{"BearerApiKey":[]}],"tags":[{"name":"tarifs","description":"Interrogation des moteurs"},{"name":"service","description":"Decouverte et etat du service"}],"paths":{"/rates":{"get":{"operationId":"getRates","tags":["tarifs"],"summary":"Tarifs et disponibilite d'un hotel pour un sejour precis","description":"Interroge le moteur derriere `url` pour le sejour demande et rend toutes les offres (chambre x tarif). C'est la route a utiliser des qu'il s'agit d'annoncer un prix : elle applique les restrictions et les packages du moteur.","parameters":[{"name":"url","in":"query","required":true,"schema":{"type":"string","format":"uri"},"description":"URL publique du moteur de reservation de l'hotel, telle qu'on la trouve sur son site. `GET /engines` liste les moteurs reconnus, `GET /resolve` dit ce qu'une URL donnee vaut.","example":"https://app.mews.com/distributor/f86e362a-8668-47dd-8e70-ac0000b6b1bd"},{"name":"checkin","in":"query","required":true,"schema":{"type":"string","format":"date"},"description":"Date d'arrivee, `YYYY-MM-DD`, dans le fuseau de l'hotel."},{"name":"checkout","in":"query","schema":{"type":"string","format":"date"},"description":"Date de depart. Alternative a `nights` ; par defaut une nuit."},{"name":"nights","in":"query","schema":{"type":"integer","minimum":1,"maximum":30},"description":"Nombre de nuits. Alternative a `checkout` ; les deux ensemble doivent concorder."},{"name":"adults","in":"query","schema":{"type":"integer","minimum":1,"maximum":12,"default":2}},{"name":"children","in":"query","schema":{"type":"integer","minimum":0,"maximum":10,"default":0}},{"name":"childrenAges","in":"query","schema":{"type":"string"},"description":"Ages des enfants, separes par des virgules (`3,9`). Exige par certains moteurs pour tarifer ; si fourni, le nombre d ages doit egaler `children`."},{"name":"rooms","in":"query","schema":{"type":"integer","minimum":1,"maximum":8,"default":1}},{"name":"currency","in":"query","schema":{"type":"string","pattern":"^[A-Za-z]{3}$","default":"EUR"},"description":"Devise demandee au moteur. Tous ne l'honorent pas : c'est `offers[].currency` qui fait foi."},{"name":"lang","in":"query","schema":{"type":"string","pattern":"^[A-Za-z]{2}$","default":"fr"}},{"name":"noCache","in":"query","schema":{"type":"string","enum":["true"]},"description":"Force une lecture amont. A n utiliser que si la fraicheur prime, c est lent et impoli."}],"responses":{"200":{"description":"Lecture aboutie — disponible ou complet.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateResult"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"502":{"$ref":"#/components/responses/Error"},"504":{"$ref":"#/components/responses/Error"}}}},"/calendar":{"get":{"operationId":"getCalendar","tags":["tarifs"],"summary":"Grille de prix par date d'arrivee","description":"Un appel couvre des dizaines de dates la ou `/rates` en couvre une : c'est la route pour observer la deformation des prix dans le temps. **Lire `support` avant les montants** : il dit d'ou vient le prix et ce qu'il vaut. Tous les moteurs ne publient pas de grille (`GET /engines` le dit a l avance).","parameters":[{"name":"url","in":"query","required":true,"schema":{"type":"string","format":"uri"}},{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date"},"description":"Premiere date d'arrivee."},{"name":"to","in":"query","schema":{"type":"string","format":"date"},"description":"Derniere date d'arrivee, incluse. Alternative a `days`."},{"name":"days","in":"query","schema":{"type":"integer","minimum":1,"default":30},"description":"Nombre de dates d'arrivee. Plafonne par `CALENDAR_MAX_DAYS` (92 par defaut)."},{"name":"nights","in":"query","schema":{"type":"integer","minimum":1,"maximum":30,"default":1},"description":"Duree evaluee a chaque date d'arrivee."},{"name":"adults","in":"query","schema":{"type":"integer","minimum":1,"maximum":12,"default":2}},{"name":"children","in":"query","schema":{"type":"integer","minimum":0,"maximum":10,"default":0}},{"name":"childrenAges","in":"query","schema":{"type":"string"}},{"name":"rooms","in":"query","schema":{"type":"integer","minimum":1,"maximum":8,"default":1}},{"name":"currency","in":"query","schema":{"type":"string","default":"EUR"}},{"name":"lang","in":"query","schema":{"type":"string","default":"fr"}},{"name":"anchors","in":"query","schema":{"type":"integer","minimum":0,"default":0},"description":"Mesure l'ecart entre la grille et `/rates` sur N dates tirees de la plage. L'ecart est publie, jamais applique : recalibrer une grille fabriquerait des prix. Chaque ancrage coute un appel amont complet."},{"name":"noCache","in":"query","schema":{"type":"string","enum":["true"]}}],"responses":{"200":{"description":"Grille lue.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalendarResult"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"},"501":{"$ref":"#/components/responses/Error"},"502":{"$ref":"#/components/responses/Error"},"504":{"$ref":"#/components/responses/Error"}}}},"/rates/batch":{"post":{"operationId":"postRatesBatch","tags":["tarifs"],"summary":"N hotels x M dates en un appel","description":"Le cas d'usage concurrentiel : « mes cinq concurrents sur trente jours ». Cache, limiteur et pool de navigateurs sont mutualises sur toute la rafale. **Les resultats sont toujours partiels plutot que globalement en echec** : chaque element porte son propre `status`, un concurrent injoignable ne fait pas tomber les autres.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchRequest"}}}},"responses":{"200":{"description":"Rafale executee. Voir `results[].status` element par element.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchResult"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error","description":"`BATCH_TOO_LARGE` : plus de `BATCH_MAX_PAIRS` couples demandes."}}}},"/engines":{"get":{"operationId":"listEngines","tags":["service"],"summary":"Moteurs supportes, et ce que vaut leur calendrier","security":[],"description":"A appeler avant `/calendar` : `calendar` est `null` quand le moteur ne publie aucune grille, ce qui evite un appel voue a `ENGINE_NOT_IMPLEMENTED`.","responses":{"200":{"description":"Liste des moteurs.","content":{"application/json":{"schema":{"type":"object","properties":{"engines":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","enum":["thais","mews","dedge","misterbooking","reservit","ihotelier","mygroomservice","louvre","siteminder","roomcloud","booking"]},"label":{"type":"string"},"mode":{"type":"string","enum":["http","browser"]},"calendar":{"oneOf":[{"$ref":"#/components/schemas/CalendarSupport"},{"type":"null"}]}}}}}}}}},"401":{"$ref":"#/components/responses/Error"}}}},"/resolve":{"get":{"operationId":"resolveUrl","tags":["service"],"summary":"Quel moteur derriere cette URL, et quel code hotel","description":"Reconnaissance seule, sans appel amont : gratuit et instantane. Utile pour verifier qu'une URL collee par un utilisateur est exploitable avant de la tarifer.","parameters":[{"name":"url","in":"query","required":true,"schema":{"type":"string","format":"uri"}}],"responses":{"200":{"description":"Moteur identifie.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EngineMatch"}}}},"401":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error","description":"`UNSUPPORTED_ENGINE` : aucune empreinte ne reconnait cette URL."}}}},"/health":{"get":{"operationId":"getHealth","tags":["service"],"summary":"Etat du service","security":[],"responses":{"200":{"description":"Etat de Redis, du navigateur et des requetes en vol."}}}}},"components":{"securitySchemes":{"ApiKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key","description":"Clef d API partagee, fournie par l exploitant du service."},"BearerApiKey":{"type":"http","scheme":"bearer","description":"Meme clef, presentee en `Authorization: Bearer <clef>`."}},"responses":{"Error":{"description":"Erreur typee. Le code dit quoi refaire : `INVALID_QUERY` corriger la requete, `BLOCKED` ou `UPSTREAM_*` reessayer plus tard, `UNSUPPORTED_ENGINE` changer d URL.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["INVALID_QUERY","UNAUTHORIZED","UNSUPPORTED_ENGINE","BATCH_TOO_LARGE","ENGINE_NOT_IMPLEMENTED","UPSTREAM_ERROR","UPSTREAM_TIMEOUT","BLOCKED","PARSE_ERROR","BROWSER_DISABLED","INTERNAL"]},"message":{"type":"string"},"details":{"type":"object","additionalProperties":true}}}}},"EngineMatch":{"type":"object","properties":{"engine":{"type":"string","enum":["thais","mews","dedge","misterbooking","reservit","ihotelier","mygroomservice","louvre","siteminder","roomcloud","booking"]},"hotelCode":{"type":"string","description":"Identifiant de l'hotel chez ce moteur."},"extra":{"type":"object","additionalProperties":{"type":"string"}},"sourceUrl":{"type":"string","format":"uri"}}},"RateQuery":{"type":"object","description":"La requete telle que le service l a normalisee. Sert de cle de cache.","properties":{"checkin":{"type":"string","format":"date"},"checkout":{"type":"string","format":"date"},"nights":{"type":"integer"},"adults":{"type":"integer"},"children":{"type":"integer"},"childrenAges":{"type":"array","items":{"type":"integer"}},"rooms":{"type":"integer"},"currency":{"type":"string"},"lang":{"type":"string"}}},"RateOffer":{"type":"object","description":"Une offre = un couple (chambre, tarif) pour la periode demandee.","required":["roomName","amount","currency","perNight"],"properties":{"roomName":{"type":"string"},"roomCode":{"type":"string"},"rateName":{"type":"string"},"rateCode":{"type":"string"},"board":{"type":"string","enum":["RO","BB","HB","FB","AI"],"description":"Pension : sans (RO), petit-dejeuner (BB), demi (HB), complete (FB), tout compris (AI)."},"refundable":{"type":"boolean"},"amount":{"type":"number","description":"Prix total du sejour, sauf si `basis` vaut `night`."},"currency":{"type":"string"},"perNight":{"type":"number","description":"`amount / nights`, arrondi au centime."},"basis":{"type":"string","enum":["stay","night"],"description":"Ce que `amount` couvre. `night` signale un moteur qui ne publie qu'un prix d appel par nuit (D-EDGE) : **ne pas multiplier**, le total du sejour serait faux."},"touristTax":{"type":"number","description":"Taxe de sejour quand le moteur l expose. Reglee sur place, hors tarif."},"roomsLeft":{"type":"integer"}}},"RateResult":{"type":"object","required":["hotel","query","available","cheapest","offers","meta"],"properties":{"hotel":{"type":"object","properties":{"engine":{"type":"string","enum":["thais","mews","dedge","misterbooking","reservit","ihotelier","mygroomservice","louvre","siteminder","roomcloud","booking"]},"hotelCode":{"type":"string"},"name":{"type":["string","null"]},"sourceUrl":{"type":"string","format":"uri"}}},"query":{"$ref":"#/components/schemas/RateQuery"},"available":{"type":"boolean","description":"`false` avec `offers: []` signifie complet — reponse valide, pas un echec."},"cheapest":{"description":"Meilleure offre, `null` si aucune. Raccourci : c'est le minimum de `offers`.","oneOf":[{"type":"object","properties":{"amount":{"type":"number"},"currency":{"type":"string"},"perNight":{"type":"number"},"basis":{"type":"string","enum":["stay","night"]}}},{"type":"null"}]},"offers":{"type":"array","items":{"$ref":"#/components/schemas/RateOffer"}},"meta":{"type":"object","properties":{"cached":{"type":"boolean"},"stale":{"type":"boolean","description":"Valeur perimee servie pendant qu un rafraichissement tourne."},"mode":{"type":"string","enum":["http","browser"]},"fetchedAt":{"type":"string","format":"date-time"},"durationMs":{"type":"integer"},"fallbackUsed":{"type":"boolean","description":"`true` quand le moteur natif a echoue et que Booking.com a pris le relais : le prix est alors un prix OTA, pas le tarif direct. A signaler a l'utilisateur."},"warnings":{"type":"array","items":{"type":"string"}}}}}},"CalendarSupport":{"type":"object","description":"Ce que vaut la grille d'un moteur, avant meme de l'interroger.","properties":{"grain":{"type":"string","enum":["best","room-rate"]},"source":{"type":"string","enum":["engine-search","engine-calendar","inventory","engine-restrictions"],"description":"`engine-search` : le moteur a vraiment tarife le sejour, vaut `/rates`. `engine-calendar` : la grille que le moteur publie lui-meme, prix d appel. `inventory` : inventaire brut, avant packages et regles d occupation — biaise vers le bas, utilisable comme forme de courbe, jamais comme prix vendable. `engine-restrictions` : aucun montant — le moteur dit ce qui est vendable, pas a quel prix ; les cases n ont pas d `amount` et seul `anchors=N` en donne."},"basis":{"type":"string","enum":["stay","night"]},"daysPerCall":{"type":"integer"},"occupancyAware":{"type":"boolean"},"losAware":{"type":"boolean"},"maxHorizonDays":{"type":"integer"},"note":{"type":"string"}}},"CalendarDay":{"type":"object","properties":{"date":{"type":"string","format":"date","description":"Date d'arrivee."},"available":{"type":"boolean","description":"`false` : rien de vendable ce jour-la. Pas une erreur."},"amount":{"type":"number"},"currency":{"type":"string"},"basis":{"type":"string","enum":["stay","night"]},"perNight":{"type":"number"},"roomCode":{"type":"string"},"rateCode":{"type":"string"},"minStay":{"type":"integer"},"closedToArrival":{"type":"boolean"},"closedToDeparture":{"type":"boolean"},"cells":{"type":"array","description":"Detail chambre x tarif, present seulement si `support.grain` vaut `room-rate`. Une case porte `packaged: true` quand son tarif comporte des articles lies que la source n a pas chiffres : ce montant-la est *sous-evalue*, se referer a /rates.","items":{"type":"object","additionalProperties":true}}}},"CalendarResult":{"type":"object","properties":{"hotel":{"type":"object","additionalProperties":true},"query":{"type":"object","additionalProperties":true},"support":{"$ref":"#/components/schemas/CalendarSupport"},"days":{"type":"array","items":{"$ref":"#/components/schemas/CalendarDay"}},"anchors":{"type":"array","description":"Ecart mesure entre la grille et `/rates`, quand `anchors` a ete demande. Publie, jamais applique.","items":{"type":"object","properties":{"date":{"type":"string","format":"date"},"calendarPerNight":{"type":["number","null"]},"exactPerNight":{"type":["number","null"]},"currency":{"type":"string"},"gap":{"type":["number","null"]},"gapPct":{"type":["number","null"]},"note":{"type":"string"}}}},"meta":{"type":"object","additionalProperties":true}}},"BatchRequest":{"type":"object","required":["urls","dates"],"properties":{"urls":{"type":"array","minItems":1,"items":{"type":"string","format":"uri"}},"dates":{"type":"array","minItems":1,"items":{"type":"object","required":["checkin"],"properties":{"checkin":{"type":"string","format":"date"},"checkout":{"type":"string","format":"date"},"nights":{"type":"integer","minimum":1,"maximum":30}}}},"adults":{"type":"integer","minimum":1,"maximum":12,"default":2},"children":{"type":"integer","minimum":0,"maximum":10,"default":0},"childrenAges":{"type":"array","items":{"type":"integer"}},"rooms":{"type":"integer","minimum":1,"maximum":8,"default":1},"currency":{"type":"string","default":"EUR"},"lang":{"type":"string","default":"fr"},"noCache":{"type":"boolean"}},"description":"Le produit cartesien `urls` x `dates` est plafonne par `BATCH_MAX_PAIRS` (200 par defaut)."},"BatchResult":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"checkin":{"type":"string","format":"date"},"checkout":{"type":"string","format":"date"},"status":{"type":"string","enum":["ok","error"]},"data":{"$ref":"#/components/schemas/RateResult"},"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}}}}}},"meta":{"type":"object","properties":{"requested":{"type":"integer"},"cached":{"type":"integer"},"fetched":{"type":"integer"},"failed":{"type":"integer"},"durationMs":{"type":"integer"}}}}}}}}