# API publique Bertel — Guide partenaires **Office de Tourisme Intercommunal du Sud de La Réunion (OTI du Sud)** · Contrat `1.0.0` · Guide mis à jour le 2026-07-03 Bertel est le système d'information touristique de l'OTI du Sud. Son API publique donne aux partenaires un accès **en lecture seule** aux fiches touristiques **publiées** du territoire : hébergements, restaurants, activités, itinéraires, événements, patrimoine, sites naturels… Elle est conçue pour la **synchronisation serveur-à-serveur** : alimenter votre site, votre application ou votre plateforme (DATAtourisme, Apidae, Tourinsoft, moteurs de recherche via schema.org) à partir des données à jour de l'OTI. --- ## 1. Démarrage rapide ### Obtenir une clé API Écrivez à **d.philippe@otisud.com** en précisant : qui vous êtes, l'usage prévu des données et le nom de votre service. Vous recevrez : - votre **clé API** au format `bk_live_` suivi de 48 caractères hexadécimaux ; - le **domaine de base** de l'API (noté `` dans ce guide). **Délai et éligibilité.** L'accès est ouvert aux partenaires touristiques et institutionnels : offices de tourisme, plateformes SIT (DATAtourisme, Apidae, Tourinsoft), agences web mandatées, éditeurs de sites et d'applications valorisant le territoire. Comptez un délai indicatif de **quelques jours ouvrés** pour l'attribution. **Évaluer avant de brancher.** Un exemple de réponse détail complet et réel est publié dans le contrat OpenAPI et la collection Postman (section 7) : de quoi juger la richesse des données sans écrire une ligne de code. Une **clé de démonstration** (lecture seule, débit limité) peut être fournie sur simple demande pour tester l'API en conditions réelles. La base de toutes les URLs est : ``` https:///api/public ``` ### Premier appel ```bash curl -H "Authorization: Bearer bk_live_votre_cle" \ "https:///api/public/objects?page_size=5" ``` Réponse (abrégée) : ```json { "meta": { "contract_version": "1.0.0", "next_cursor": "eyJ…" }, "data": [ { "id": "RESRUN00000000XK", "type": "RES", "name": "Le Macabit", "…": "…" } ] } ``` --- ## 2. Authentification, limites et versionnage ### Clé API Chaque requête porte la clé dans l'en-tête HTTP : ``` Authorization: Bearer bk_live_… ``` Clé absente, inconnue, révoquée ou expirée : réponse `401 {"error":"unauthorized"}`. > **Votre clé est un secret serveur.** Ne l'embarquez jamais dans du code exécuté chez l'utilisateur final (page web, application mobile) : toute personne inspectant le trafic la récupérerait. Appelez l'API depuis vos serveurs uniquement. En cas de fuite, signalez-la : la clé est révocable immédiatement. ### Limite de débit **120 requêtes par minute** et par clé (fenêtre fixe). Au-delà : `429 {"error":"rate_limited","retry_after":N}` avec l'en-tête `Retry-After` (secondes). Attendez ce délai avant de réessayer. Chaque réponse porte les en-têtes `X-RateLimit-Limit` (le plafond, 120) et `X-RateLimit-Remaining` (requêtes restantes dans la fenêtre courante, `0` sur un `429`) : surveillez `X-RateLimit-Remaining` pour vous auto-réguler avant d'atteindre la limite. ### Enveloppe et versionnage Seule une réponse `200` a la forme `{ "meta": …, "data": … }` ; les erreurs sont des corps **plats** portant une clé `error` (voir la table des codes d'erreur). L'en-tête `X-Bertel-Api-Version` reste présent **partout**, y compris sur les erreurs, et la réponse `200` porte en plus la version de contrat dans `meta.contract_version`. Au sein d'une même version majeure, les évolutions sont **uniquement additives** : de nouvelles clés peuvent apparaître, les clés existantes ne changent jamais de sens. **Votre parseur doit donc ignorer les clés inconnues.** Une rupture de contrat serait publiée sous `/api/public/v2/*`, sans casser la v1. ### Codes d'erreur | Code | Corps | Signification | |---|---|---| | 400 | `invalid_object_id` / `bad_request` | Paramètre malformé (id hors format, `since` non ISO 8601, code `types` inconnu) | | 401 | `unauthorized` | Clé absente, invalide, révoquée ou expirée | | 404 | `not_found` | Fiche inconnue **ou non publiée** (indistinguable, par conception) | | 429 | `rate_limited` | Limite de débit atteinte — respecter `Retry-After` | | 500 / 502 | `server_misconfigured` / `upstream_error` | Erreur côté Bertel — réessayer plus tard avec backoff | --- ## 3. Les endpoints ### 3.1 `GET /objects` — liste paginée des fiches publiées | Paramètre | Type | Défaut | Description | |---|---|---|---| | `cursor` | string | — | Curseur opaque de pagination (voir ci-dessous) | | `page_size` | int 1–200 | 50 | Nombre de fiches par page (borné à **100** en `view=full`) | | `view` | `card` \| `full` | `card` | `full` = fiche complète par objet (voir §3.1.1) | | `track` | `gpx` \| `kml` | — | En `view=full` : ajoute le blob GPX/KML des itinéraires | | `types` | csv | tous | Filtre par types de fiche (codes ci-dessous) | | `search` | string | — | Recherche sur le **nom** de la fiche uniquement | | `lang` | code | `fr` | Langue de résolution des textes | | `format` | profil | — | Ajoute un document pivot par fiche (voir section 5) | **Pagination par curseur** : la réponse porte `meta.next_cursor`. Repassez cette valeur telle quelle dans `?cursor=` pour obtenir la page suivante ; `next_cursor` vaut `null` sur la dernière page. Arrêtez la boucle de pagination quand `next_cursor` vaut `null`. Le curseur est **opaque** — ne le construisez ni ne l'interprétez jamais. Les paramètres de filtre (`types`, `page_size`, `search`, `lang`, `view`, `track`) sont **figés par le curseur** : pour changer un filtre, repartez sans `cursor`. **Codes de type** (paramètre `types`, champ `type` des fiches) : | Code | Libellé | Code | Libellé | |---|---|---|---| | HOT | Hôtel | PCU | Patrimoine culturel | | HLO | Gîte & meublé | PNA | Site naturel | | RVA | Résidence de vacances | ITI | Itinéraire / sentier | | HPA | Hôtellerie de plein air | FMA | Fête / manifestation | | CAMP | Camping | VIL | Ville / village | | RES | Restaurant | PRD | Producteur | | ASC | Activité | COM | Commerce | | ACT | Activité encadrée | PSV | Prestataire de services | | LOI | Loisir | SPU | Service public | | ORG | Organisation | | | Les codes sont **insensibles à la casse** (`res` équivaut à `RES`). Un code **inconnu** fait échouer toute la requête avec `400 {"error":"bad_request"}` (le champ `detail` nomme le ou les codes fautifs) — vérifiez l'orthographe contre la liste ci-dessus plutôt que de réessayer. Exemple — les restaurants et hôtels, 200 par page : ```bash curl -H "Authorization: Bearer bk_live_…" \ "https:///api/public/objects?types=RES,HOT&page_size=200" ``` #### 3.1.1 Objets complets en liste — `view=full` Par défaut la liste renvoie une **carte allégée** par objet. Avec `view=full`, chaque objet de la page est la **fiche complète** — strictement le même contenu que `GET /objects/{id}` (photos, descriptions, équipements, classements/distinctions, chambres, horaires, `itinerary_details.track_geojson`…), **hors CRM et champs internes**. - La page est bornée à **100 objets** (défaut 25) : la fiche complète est ~15 kB et ~30 ms par objet, donc une grande page reste volumineuse. Utilisez le curseur pour parcourir. - Pour les **itinéraires**, ajoutez `track=gpx` (ou `kml`) pour obtenir en plus le blob de tracé au format demandé sous `itinerary_details.track`. Le tracé **GeoJSON** est de toute façon inclus nativement (`itinerary_details.track_geojson`). ```bash curl -H "Authorization: Bearer bk_live_…" \ "https:///api/public/objects?view=full&page_size=100&track=gpx" ``` ### 3.2 `GET /objects/{id}` — une fiche complète L'identifiant a la forme `RESRUN00000000XK` (3 lettres de type + territoire + 10 caractères). Une fiche inconnue, dépubliée ou archivée répond `404` — un partenaire ne voit **jamais** un brouillon. | Paramètre | Description | |---|---| | `lang` | Langue de résolution (`fr` par défaut) | | `lang=all` | Résout en français **et** ajoute un bloc `data.i18n` = `{champ: {langue: texte}}` avec toutes les traductions disponibles, en un seul appel. Ce bloc couvre **uniquement** les 7 champs de la famille description (`description`, `description_chapo`, `description_mobile`, `description_edition`, `description_adapted`, `description_offre_hors_zone`, `sanitary_measures`) — **pas** le nom (`name`) ni les proses de facettes (menus, chambres, étapes d'itinéraire) | | `format` | Ajoute un document pivot (section 5) | ```bash curl -H "Authorization: Bearer bk_live_…" \ "https:///api/public/objects/RESRUN00000000XK?lang=all" ``` La fiche `data` contient l'ensemble des données publiques : identité, descriptions (texte propre), localisation et géo, contacts publics, médias publiés, horaires d'ouverture, tarifs, capacités, classements et labels, etc. **Blocs de la fiche** — les blocs toujours présents décrivent l'identité commune à tous les types ; les blocs conditionnels n'apparaissent que pour les types qui les portent (colonne « Conditionnel »). | Bloc | Contenu | Conditionnel | |---|---|---| | `identite` | `id`, `type`, `name`, `status`, `region_code`, dates | toujours | | `address` | Adresse postale | toujours | | `location` | Latitude / longitude | toujours | | `description` + `descriptions[]` | Textes descriptifs (+ variantes `_md` en Markdown) | toujours | | `contacts` | Canaux de contact publics | toujours | | `web_channels` | Réseaux sociaux et canaux web publics | toujours | | `media` | Photos / vidéos publiées (champ `credit`) | toujours | | `capacity` | Capacités d'accueil | toujours | | `amenities` | Équipements | toujours | | `environment_tags` | Tags d'environnement | toujours | | `payment_methods` | Moyens de paiement | toujours | | `prices` | Tarifs | toujours | | `discounts` | Réductions | toujours | | `group_policies` | Conditions groupes | toujours | | `classifications` | Labels et classements | toujours | | `taxonomy` | Classement taxonomique | toujours | | `tags` | Étiquettes libres | toujours | | `languages` | Langues parlées | toujours | | `opening_times` | Périodes d'ouverture (`periods`) | toujours | | `org_links` | Organisations rattachées | toujours | | `actors` | Acteurs (opérateurs, contacts) | toujours | | `legal_records` | Mentions légales | toujours | | `sustainability_labels` / `sustainability_actions` | Labels et actions de durabilité | toujours | | `accessibility_labels` | Labels d'accessibilité | toujours | | `outgoing_relations` / `incoming_relations` | Relations entre fiches | toujours | | `render` | Lignes pré-formatées en français | toujours | | `activity` | Détail activité | ACT / ASC | | `itinerary` + `itinerary_details` | Détail itinéraire | ITI | | `menus` + `cuisine_types` + `dietary_tags` + `allergens` | Carte, types de cuisine, régimes, allergènes | RES / FMA | | `menu_documents` | Documents de carte | RES | | `fma_occurrences` | Dates d'occurrence de l'événement | FMA | | `room_types` + `meeting_rooms` | Chambres et salles de séminaire | Hébergement / MICE | | `places` | Sous-lieux | selon la fiche | Un exemple de réponse détail complet et réel est fourni dans le contrat OpenAPI (`openapi.json`) et la collection Postman. **Langues.** Le code de résolution par défaut est `fr`. Le corpus est aujourd'hui majoritairement francophone : la traduction du contenu des établissements est un chantier en cours. À défaut de traduction disponible pour un champ dans la langue demandée, c'est la valeur française qui est renvoyée. ### 3.3 `GET /objects/deletions` — flux des suppressions (tombstones) Quand une fiche est **définitivement supprimée** de Bertel, elle disparaît simplement de `GET /objects`. Ce flux vous permet de propager ces suppressions chez vous. | Paramètre | Type | Défaut | Description | |---|---|---|---| | `since` | ISO 8601 | — | Ne renvoyer que les suppressions strictement postérieures ; absent = tout l'historique | | `limit` | int 1–1000 | 500 | Taille de page | ```json { "meta": { "contract_version": "1.0.0", "cursor": "2026-07-01T08:12:44.000Z", "count": 2 }, "data": [ { "object_id": "LOIRUN000000000A", "type": "LOI", "deleted_at": "2026-06-30T14:03:21.000Z" } ] } ``` Conservez `meta.cursor` et repassez-le en `since` à l'appel suivant (il reste inchangé quand la page est vide). > **Dépublication ≠ suppression.** Une fiche dépubliée (repassée en brouillon ou archivée) n'apparaît **pas** dans ce flux : elle disparaît de la liste `GET /objects` et son détail répond `404`. Voir la réconciliation en section 4. ### 3.4 `GET /catalog` — vocabulaires contrôlés Les fiches référencent des codes (équipements, types de cuisine, labels…). Ce endpoint fournit les catalogues correspondants. | Paramètre | Description | |---|---| | `domains` | csv de domaines ; absent = **tous** les catalogues publics (les clés de la réponse = liste des domaines disponibles) | | `lang` | Langue des libellés (`fr` par défaut) | Réponse : `data` = `{ "": [{ "code", "name", "icon_url", "parent_code", "domain" }] }`. Un domaine inconnu est ignoré silencieusement. --- ## 4. Recette de synchronisation complète Synchronisation initiale, puis incrémentale à chaque cycle (par exemple quotidien) : ```text # 1. Upserts — parcourir la liste complète cursor = absent répéter : r = GET /objects?page_size=200[&format=][&cursor=cursor] pour chaque fiche de r.data : créer ou mettre à jour chez vous (upsert par id) cursor = r.meta.next_cursor tant que cursor n'est pas null # 2. Suppressions définitives d = GET /objects/deletions?since= retirer chez vous chaque d.data[i].object_id ; conserver d.meta.cursor # 3. Réconciliation des dépublications tout id présent chez vous mais absent du parcours (1) ET du flux (2) ⇒ fiche dépubliée : la masquer ou la retirer chez vous ``` > **Fiches complètes en une passe :** à l'étape 1, `GET /objects?view=full&page_size=100[&cursor=cursor]` > renvoie la fiche complète par objet (au lieu d'un appel détail par fiche). La page est bornée à > **100** en mode `full`. Ajoutez `&track=gpx` pour les tracés GPX des itinéraires. Bonnes pratiques : - `page_size=200` minimise le nombre d'appels (la base compte moins de 1 000 fiches publiées : la synchro complète tient en quelques requêtes). - Restez sous la limite de débit ; sur `429` ou `5xx`, réessayez avec un backoff (attendre `Retry-After`, puis doubler). - L'ordre de la liste n'est pas contractuel : l'upsert par `id` est la seule stratégie fiable. - Les fiches sont mises à jour en continu par l'OTI : comparez le champ `updated_at` pour ne retraiter que les fiches réellement modifiées depuis votre dernier cycle. --- ## 5. Formats pivot — `?format=` Chaque fiche peut être servie accompagnée d'un document conforme à un standard du secteur. Le paramètre `format` fonctionne sur le **détail** et sur la **liste** (le document est alors attaché à chaque élément de la page — même pagination, aucun appel supplémentaire). | `?format=` | Clé ajoutée | Standard | Usage type | |---|---|---|---| | `jsonld` | `jsonld` | schema.org (JSON-LD) | SEO — à coller dans un `