Repository navigation
API
Statut : 🟡 Brouillon · Màj : 2026-10-14 · Tickets : #164 #306 #378 #383 #393 #397
Contrat du MVP. Préfixe /v1. Authentification par jeton Firebase quand indiqué. Les schémas Zod dans packages/shared font foi ; cette page en est le résumé lisible.
- Pagination par curseur :
?cursor=&limit=(max 50), réponse{ items, nextCursor }. - Listes dans la query : clé répétée, avec ou sans crochets (
moods[]=food&moods[]=cultureoumoods=food&moods=culture) ; les deux notations se mélangent et les doublons sont retirés. Toute autre notation (moods[0]=) est une clé inconnue. Les queries sont strictes : une clé inconnue (mood=,moods[0]=) ou une valeur hors taxonomie (budgets[]=premium) répond 400validation_error, avecdetails.issues; jamais un résultat non filtré (#25, D-066). Règle du code HTTP devalidation_error(D-071, #221) : 400 pour une query invalide (GET), 422 pour un corps invalide (POST, PATCH, PUT). - Erreurs :
{ code, message, details? }, codes HTTP standards. Codes métier stables :validation_error,unauthorized,forbidden,not_found,conflict,rate_limited,coherence_blocked,link_expired(en 410 : lien de confirmation expiré, déjà utilisé ou annulé, D-062),premium_required(en 402, avecdetails.reasonpour alimenter E-17 :premium_routeoucreate_limit, etdetails.limitpour la création ; D-023) ; brouillon #393, à valider :route_removed(en 410 : parcours retiré, avecdetails.removedAtetdetails.near, voir Fiche parcours).
🟡 Brouillon #383 du 2026-10-14, à valider par Ilan, voir Compte-et-Monetisation › Fin de la limite de création : proposé,
premium_requiredne garde quepremium_route(create_limitetdetails.limitde la création retirés) ;rate_limitedportedetails.reason(pending_routes) etdetails.limitpour la limite de relecture (D-073).
- Dates ISO 8601 UTC ; coordonnées
{ lat, lng }. - Toutes les lectures publiques sont cachables (
Cache-Controlcourt, ETag), pour l'appelant anonyme. - Authentification facultative (D-075) : une route publique dont la réponse dépend du droit de l'appelant (aujourd'hui
/routes/search) accepte un jeton Firebase sans l'exiger. Sans en-têteAuthorization, réponse anonyme,Cache-Control: public. Avec un en-têteAuthorization, réponseCache-Control: privateetVary: Authorization, jamais servie à un autre appelant. Un jeton présent mais invalide ou expiré répond 401unauthorized, comme sur une route authentifiée : l'API ne retombe pas en silence sur la réponse anonyme, pour que l'app rafraîchisse son jeton et réessaie.
| Méthode | Route | Description |
|---|---|---|
| GET | /routes/search |
Parcours dans une zone avec filtres. Query : bbox=w,s,e,n, audiences[], moods[], conditions[], durations[], budgets[], transports[] (valeurs de la taxonomie), `sort=recommended |
| GET | /users/:id/profile |
Profil public (D-023) : avatar, prénom, membre depuis, quartier, bio, certified et certification (critères avec valeur, seuil et état, pour la sheet d'explication), chiffres routesPublished, routesCompleted (sorties sur ses parcours, D-091), ratingAvg (chacun null s'il n'existe pas), verifiedPlaces (lieux distincts confirmés sur place ou après la sortie, null à zéro, D-056), anecdotes dans l'ordre (questionKey, libellé, réponse), parcours à la une dans l'ordre (cards), parcours publiés (cards ; query `sort=recent |
| GET | /routes/:id/ratings |
Avis visibles, paginés |
| GET | /routes/search/count |
Même query, réponse { count } pour le compteur des filtres. breakdown=all_but_one ajoute without : le nombre de parcours obtenu en retirant chaque filtre actif pris isolément, groupe par groupe et jamais valeur par valeur, pour les suggestions du zéro résultat de E-03 (D-010) |
| GET | /routes/:id |
Fiche complète (étapes, lieux, horaires du jour, computed, vérification, lieux signalés ; pour l'utilisateur connecté, myReport : date et statut de son signalement en cours, D-057). ?token= pour un parcours privé. Trajets à pied, à vélo et en voiture : distances, durées et tracés exacts demandés à Mapbox Directions pour cette réponse, jamais enregistrés ; nos estimations si Mapbox ne répond pas (estimated, sans tracé, D-088). Parcours access = premium pour un appelant non abonné : étape 1 complète, étapes suivantes réduites à heure, catégorie, durée et trajet, sans lieu, coordonnées ni conseil, et pas de tracé (D-014). Parcours retiré : 410 avec la date du retrait. Contrat détaillé, en brouillon : Fiche parcours
|
| GET | /routes/:id/timeline |
Query startsAt= : timeline recalculée et conflits, sur les trajets exacts de Mapbox quand il répond, nos estimations sinon (D-088). Le brouillon #393 propose nos estimations seules (Q6) : Fiche parcours
|
| GET | /geocode |
Query q=, proxy Mapbox (Geocoding et Search Box pour les stations), interrogé dans l'emprise de l'Île-de-France ; une zone n'est gardée que si chaque mot tapé commence un mot de son nom. Sans cache (résultats temporaires de Mapbox, qu'il est interdit de conserver) : Cache-Control: no-store. Limité à 30 requêtes par minute et par IP. Le géocodage permanent, qui permettrait un cache de 24 h, est écarté pour l'instant, question ouverte (D-081, voir Recherche textuelle) |
| GET | /weather/today |
Query city=paris : `{ condition: 'rainy' |
| GET | /config |
Taxonomies, libellés, seuils publics (dont ceux de la certification), questions d'anecdote actives (clé, libellé, ordre), versions de l'app par plateforme : minVersion (sous elle, E-20 bloque) et recommendedVersion (sous elle, E-20 propose la mise à jour, D-063) ; versions en vigueur des textes légaux (legal.terms, legal.privacy) |
| GET | /legal/:doc |
doc = terms ou privacy, `?locale=fr |
| POST | /support/messages |
Formulaire « Nous écrire » (E-19, D-063), avec ou sans jeton : `{ topic: 'question' |
Card de /routes/search (RouteCard dans packages/shared), telle que livrée par #25, #161, #63 et #170 (D-066), révisée pour les parcours Premium par D-075 :
-
id,title,coverUrl(photo de couverture du parcours),isOfficial,author(id,firstName,avatarUrl;nullpour un parcours « Par Widoo », et quand l'auteur est supprimé ou son profil non public, D-025),access(free,premium),isVerified,moods(la première est l'ambiance principale),audiences,districtetneighborhooddu départ (« 3e », « Le Marais »),durationMinetdurationBucket,distanceM,rating(average,nullavant le premier avis, etcount). -
budgetPerPersonEur: somme exacte des coûts des étapes par personne, en euros, un seul nombre ; plus de fourchette (D-032, #170). L'app l'arrondit à l'affichage (« ≈ 25 € par pers. », « Gratuit » à zéro).budgetBucketest dérivé de la même somme exacte, jamais de l'arrondi. -
isLocked:truequand la card d'un parcoursaccess = premiumest verrouillée pour l'appelant,falsesinon (toujoursfalsepour un parcours gratuit). L'app lit ce champ, jamaisaccessseul, pour choisir l'affichage : une card Premium déverrouillée s'affiche comme un parcours gratuit, avec sa pastille Premium (D-075). -
stepCount: nombre d'étapes du parcours, exact, questepsles porte toutes ou non. -
steps[], dans l'ordre, pour les pastilles et la tooltip de la carte (E-04) :category(valeur deplaces.category),location({ lat, lng }),name(nom du lieu) etdurationMin(durée sur place, pour « Étape i/n · catégorie · durée »). Parcours gratuit : toutes les étapes. Parcoursaccess = premium: la règle d'E-04 (« Abonné, le tap validé ne change pas ») se tient dans l'API (D-014, D-075).- Card verrouillée (
isLocked = true), sans jeton ou pour un compte qui n'a pas droit au parcours : le départ seul, avec sa catégorie et ses coordonnées,nameetdurationMinànull; les étapes 2 et suivantes ne quittent pas l'API, ni nom, ni lieu, ni coordonnées, ni photo. La tooltip Premium litstepCountetdurationMindu parcours (« 4 étapes en 7 h ») ; la carte se recadre sans tracé ni étapes. - Card déverrouillée (
isLocked = false), avec le jeton valide d'un compte qui a droit au parcours : toutes les étapes, comme un parcours gratuit ; la carte affiche tracé, étapes et tooltips. - Droit au parcours : aujourd'hui, un compte dont
users.planvautpremiumet dontusers.plan_expires_atn'est pas passé. Le pass 7 jours (même colonnes) et le déblocage à l'unité (route_unlocks) entrent dans la même règle par #66, calculée en un seul endroit du code et partagée avec/routes/:id(#36). - Écarté : réduire la card pour tout le monde au nom du cache public (choix de #161, refusé le 2026-09-29) ; faire attendre l'abonné jusqu'à l'ouverture de la fiche.
- Card verrouillée (
- Photo de la ligne d'étape de la tooltip : la maquette montre la photo du lieu de l'étape (E-04), que la card ne porte pas ; l'app affiche aujourd'hui
coverUrl. Décision (D-066) : la card gagnesteps[].photoUrl(photo principale du lieu en vignette,nullsans photo ; présente seulement sur les étapes que la card porte, donc absente des étapes 2 et suivantes d'une card verrouillée, D-075), la tooltip retombant surcoverUrlquand elle est nulle ; nécessaire à la tooltip d'une étape touchée (#153), dont le lieu n'est pas celui de la couverture. Confirmé par Ilan le 2026-09-28 (D-071) ; photo principale d'un lieu : voir Modele-de-Donnees › media ; livré par #219. -
reason: raison principale du classement, avecsort=recommendedseulement (absente avec les autres tris) :{ key: 'near_you', distanceM }, ou{ key }parmitop_rated,verified,fresh,good_timing,official;nullquand aucune composante ne se dégage, et l'app n'affiche alors rien. Clés techniques ; les libellés sont dans l'app, qui n'affiche quegood_timingetfresh(D-071). Règles : Filtres-et-Recherche › Recommandation (#63).
Statut : 🟡 Brouillon du 2026-10-14, à valider par Ilan, en réponse à #393. Rien ici n'est encore décidé : chaque choix ouvert est une question numérotée (Questions à trancher), avec la recommandation et ce qu'elle écarte. Après validation : entrée
D-xxxau Design-Changelog, report par MANAGER dans #36.
Contrat de GET /routes/:id (la fiche, Ecrans › E-05) et de GET /routes/:id/timeline. Point de départ : le schéma RouteDetail de packages/shared/src/schemas/route-detail.ts, fixé par la PR #390 (#37) ; cette section en est le résumé lisible et liste ce que #36 doit y ajouter. L'endpoint tient ce schéma partagé, sans seconde définition.
Authentification facultative, comme /routes/search (voir Conventions, D-075) : la réponse dépend de l'appelant (droit Premium, myReport). Un jeton Firebase invalide ou expiré répond 401 unauthorized. Le droit au parcours entier se décide par canAccessRoute(route, viewer, now) (apps/api/src/access/route-access.ts), la règle unique déjà lue par la recherche et la programmation, sans seconde implémentation.
| Statut du parcours | Sans jeton de lien | Avec ?token= valide (routes.share_token) |
Auteur connecté |
|---|---|---|---|
published, needs_fix
|
200 | 200, jeton ignoré | 200 |
private |
404 not_found
|
200, avec sharedBy
|
200 |
pending, changes_requested, jamais publié |
404 | 200, avec sharedBy (Q4) |
200 |
pending, changes_requested, déjà publié (un needs_fix corrigé et resoumis, PR #420) |
404 (Q3) | 404 (Q3) | 200 |
draft, rejected
|
404 | 404 | 404 : l'auteur les lit par GET /me/routes/:id
|
unpublished |
410 route_removed
|
410 | 410 : l'auteur le rouvre par GET /me/routes/:id
|
| inexistant, ou supprimé avec le compte de son auteur (D-025) | 404 | 404 | — |
- Identifiant mal formé : 400
validation_error(D-071). Jeton faux ou d'un autre parcours : 404, jamais 403, pour ne pas confirmer qu'un parcours privé existe. - Pas d'exception de rôle : un modérateur lit un parcours par
GET /admin/routes/:id. - Auteur supprimé : un parcours publié anonymisé reste servi,
authorànull(« Membre Widoo », D-025).
Query : token (lien privé) ; trips=estimated (nos estimations seules, sans appel à Mapbox ni tracé, pour le site, Q9) ; toute autre clé : 400 validation_error.
RouteDetail, tel que l'appelant a le droit de le voir :
-
Identité : champs de la card (
id,title,coverUrl,isOfficial,author,access,isVerified,moods,audiences,conditions,transport,district,neighborhood,rating,stepCount), plusstatus(publishedouneeds_fixpour le public),description,photoUrls(couverture d'abord). -
Chiffres (issus de
computed, sans exposer l'objet brut) :durationMin,durationBucket,distanceM,walkingDistanceM,effort,budgetPerPersonEuretbudgetBucket,startAccess(station la plus proche du départ, D-050,nulltant qu'elle n'est pas calculée). Le budget est la somme exacte, comme sur la card ; l'app l'arrondit (« ≈ 25 € par pers. », D-032, D-066 ; Q11). Les paliers restent ceux enregistrés ; durée, distance, marche et effort sont ceux des trajets de la réponse (Q6). -
Créateur :
creator(routesPublished,isCertified, D-023) ;sharedBy(firstName) pour un parcours privé ouvert par son lien (« Partagé par Camille »),nullsinon. -
Avis :
reviews, les trois derniers avis visibles, du plus récent au plus ancien (prénom, note, commentaire, date ; sans réponse) ; la liste complète estGET /routes/:id/ratings. -
Timeline :
timeline, celle du départ conseillé, aujourd'hui dans la ville (« Départ conseillé à 10:00 ») :routes.suggested_start, sinon le départ calculé (D-050). Même formeRouteTimelineque celle dePlannedRoute(GET /me/planned, PR #401) :{ startsAt, endsAt, steps: [{ stepId, arrivesAt, leavesAt, conflicts }] }, une entrée par étape dans l'ordre des étapes, conflits d'Orchestration › Timeline (closed_at_arrival,closed_that_day,closes_during_step,hours_unknown). -
Étapes :
steps, toutes, dans l'ordre (steps.length = stepCount).- Étape complète (
isLocked: false) : titre, description (le conseil du créateur), durée, coût, réservation (bookingRequired,bookingUrl,bookingProvider,bookingNote,bookingIsAffiliate, D-048, D-052),endsOn(étape éphémère, D-050),transitionNote, le lieu (name,category,location,address,isIndoor,photoUrl,verificationStatus,verifiedAt,hours) et la vérification{ confirmations, lastConfirmedAt, reports: { count, lastAt } | null }(D-013, D-056 ; « Pas encore vérifié » quandlastConfirmedAtest nul). - Étape verrouillée (
isLocked: true,LockedStepstrict) :id,position,category,durationMin,transitionseulement. Le schéma refuse tout autre champ : ni nom, ni lieu, ni coordonnées, ni conseil, ni photo ne quittent l'API (D-014). - Trajet vers l'étape suivante (
transition,nullpour la dernière) :distanceM,durationMin,mode,estimated, etgeometry, à ajouter au schéma pour #154 : le tracé dans les rues en polyline encodée (précision 5, ce queapps/api/src/directions/mapbox.tsdemande déjà),nullsans tracé.
- Étape complète (
-
Pour l'utilisateur connecté :
myReport, date de son signalement du parcours s'il date de moins de 7 jours et n'est pas tranché,nullsinon (D-057) ; absent ounullsans jeton.
Trajets exacts, rien d'enregistré (D-088). À chaque GET /routes/:id d'un parcours que l'appelant voit en entier, l'API demande à Mapbox Directions les trajets à pied, à vélo et en voiture (profils walking, cycling, driving) pour cette réponse seulement : estimated: false, distance, durée et geometry de Mapbox. Rien n'est écrit en base (computed, steps.transition, direction_cache restent sur nos estimations), ni journalisé, ni mis en cache (voir En-têtes). Repli, jamais une erreur : Mapbox en échec, trop lent (4 s par appel, comme aujourd'hui) ou quota atteint, la réponse porte l'estimation enregistrée, estimated: true, geometry: null. Le métro n'a pas de profil Mapbox : toujours notre estimation, estimated: true, sans tracé (ligne droite dans l'app, #154). Piste de #36 : une requête par profil et par parcours, les étapes en points de passage (25 au plus), pour tenir la limite de 300 requêtes par minute.
Parcours Premium sans droit (isLocked: true, D-014, D-075) : étape 1 complète, étapes suivantes en LockedStep ; aucun tracé (geometry: null partout) ; aucun appel à Mapbox, trajets sur nos estimations (Q7) ; conflits de la timeline vides sur les étapes verrouillées, qui ne disent rien des horaires d'un lieu caché (Q7). La fiche reste lisible : photos, titre, chiffres, description, infos, créateur, avis.
Avertissements utiles au lecteur (Q10) : la fiche ne porte ni warnings ni errors de cohérence, réservés à l'éditeur (OwnRoute, PR #420) et à la modération ; « le score de cohérence n'est pas montré à l'utilisateur final » (Orchestration). Le lecteur voit ce qui le concerne par des champs déjà là : status = needs_fix (bandeau), les conflits de la timeline, la vérification et verificationStatus de chaque lieu (lieu signalé, stale, closed), endsOn d'une étape (« Une étape s'est terminée le 17 janvier »).
À ajouter au schéma de #390 par #36, décisions déjà prises que route-detail.ts ne porte pas encore :
-
geometryde chaque trajet (D-088, #154) ; -
myReport(D-057) ; - « À prévoir » : lignes du créateur (
routes.tips) et suggestions calculées non masquées (computed, moinsroutes.tips_hidden), D-048, D-050, lues par E-06 ; - crédit des photos libres (
author,license,sourceUrl, D-052) :photoUrlsetplace.photoUrldeviennent des objets photo,nullpour une photo de créateur ; -
timezonede la ville (fuseau IANA, commePlannedRouteSnapshot) : les horaires sont des heures locales, et « Ouvert aujourd'hui » se lit à l'heure de Paris, pas à celle du téléphone.
Hors de #36 : la traduction (route_translations, D-076), qui ajoutera sa langue d'origine et ses champs traduits avec son propre ticket ; l'état « déjà programmé » de la barre (D-035), lu sur GET /me/planned.
- Query :
startsAtobligatoire, date et heure ISO 8601 avec décalage, même format quePOST /me/planned;tokenpour un parcours privé. Clé inconnue ou valeur invalide : 400validation_error. - Mêmes règles d'accès que la fiche (404, 410, jeton). Parcours Premium sans droit : 402
premium_required,details.reason: premium_route, commePATCH /me/planned/:idsurstartsAt(D-038) : sans droit, on ne programme pas (Q7). - Réponse :
RouteTimelinepour ce départ, même forme quetimelinede la fiche et dePlannedRoute, horaires valables à la date demandée (horaires saisonniers, jour de fermeture), conflits compris. - Trajets : nos estimations, celles que
POST /me/plannedfige dans la programmation, pour que l'heure de fin vue dans E-06 soit celle de la sortie enregistrée ; aucun appel à Mapbox (Q6, qui amende la ligne du tableau ci-dessus). - Ce qui reste à
/timeline: un autre départ que le départ conseillé (E-06 « Autre date », changement d'heure). La fiche n'a pas besoin de l'appeler à l'ouverture : sa timeline est dansRouteDetail.
Un parcours unpublished (retiré par son auteur, par l'équipe, ou dépublié parce qu'un de ses lieux a fermé définitivement) répond, à tout appelant et avec ou sans jeton :
HTTP/1.1 410 Gone
{
"code": "route_removed",
"message": "Route removed",
"details": {
"removedAt": "2026-09-20T08:12:00.000Z",
"near": { "lat": 48.86, "lng": 2.35 }
}
}- Code stable
route_removed, à ajouter à la liste des Conventions ; il figure déjà dansapiErrorCodes(packages/shared/src/schemas/error.ts), etdetailsest validé côté app parRouteRemovedDetails, auquel s'ajoutenear. Corps uniqueApiErrord'apps/api/src/errors.ts(code,message,details), qui demande unapiCodeexplicite commecoherence_blocked, 410 n'ayant pas de code par défaut danscodeByStatus. -
removedAt: date du passage enunpublished. Aucune colonne ne la porte aujourd'hui :routes.unpublished_at(timestamptz), posée par l'écrivain unique du statut (moveRoute) à chaque passage enunpublished, remise ànullà la republication (Q2). -
near: la zone du parcours, pour que « Voir des parcours proches » recentre l'accueil (E-05) : les coordonnées du départ arrondies à deux décimales (environ 1,1 km du nord au sud, 0,7 km d'est en ouest à Paris), jamais le point exact (Q1). - Rien d'autre du parcours retiré : ni titre, ni auteur, ni étapes, ni motif du retrait.
- La 410 vaut tant que la ligne du parcours existe ; un parcours supprimé avec le compte de son auteur répond 404 (seuls les parcours publiés sont gardés, anonymisés, D-025). Le site (
/r/:id, E-21) rend sur la 410 l'encart « Ce parcours n'est plus disponible », sans lirenear. - Écart avec la planche parcours retiré : elle dit « Son créateur l'a retiré le 20 sept. », alors qu'un retrait peut venir de l'équipe ou d'un lieu fermé (Q2).
-
GET /routes/:id, réponse 200 :Cache-Control: no-store, sans ETag (Q8). Elle porte des résultats de Mapbox Directions, qu'il est interdit de conserver (D-088), comme/geocode(D-081) ; et elle dépend de l'appelant. L'app garde la fiche ouverte en mémoire pendant qu'on la lit, sans la persister. - Limite : 60 requêtes par minute et par IP sur
GET /routes/:id, chaque ouverture coûtant une requête Mapbox par profil, voire par trajet (menace « abus des API tierces payantes », Securite-et-RGPD) ; 429rate_limitedau-delà (Q8). -
GET /routes/:id/timeline, sans données Mapbox : comme/routes/search,Cache-Control: public(60 s, ETag) sansAuthorizationnitoken;private, no-cacheetVary: Authorizationavec un jeton Firebase ;private, no-storeavec untokende lien privé (règle de/r/:id, Site-Web). - Erreurs (400, 401, 402, 404, 410, 429) : jamais mises en cache, comme aujourd'hui (
cacheHook). - Site
widoo.app: son cache de 5 minutes sur/r/:idne peut pas garder de résultat Mapbox ; il lit la fiche avec?trips=estimated, sans appel à Mapbox, réponse alors cachable comme/timeline(Q9).
Pour Ilan. Chaque question porte la recommandation, puis ce qu'elle écarte.
-
Zone dans la 410. La 410 porte-t-elle un point pour « Voir des parcours proches » ? Recommandé : oui,
details.near, départ arrondi à 0,01° (environ 1 km), assez pour recentrer la carte de l'accueil sur le quartier, sans désigner un lieu. Écarte : le départ exact (il nommerait le premier lieu d'un parcours peut-être retiré pour « Lieu dangereux ou interdit d'accès ») ; l'arrondissement en texte (l'app devrait le géocoder par Mapbox, et « 3e » seul n'a pas de centre) ; l'emprise du parcours (elle trace sa forme) ; rien (l'accueil s'ouvrirait sur la position de l'utilisateur, alors qu'E-05, validée, dit « accueil centré sur sa zone »). -
Date et auteur du retrait. Recommandé : nouvelle colonne
routes.unpublished_at, et un texte neutre sur la planche, « Ce parcours a été retiré le 20 sept. », sans dire qui l'a retiré. Écarte :details.removedBy(authorouteam), qui dirait publiquement que l'équipe a retiré le parcours d'un créateur nommé ailleurs ; garder « Son créateur l'a retiré » pour tous les retraits (faux pour un retrait de l'équipe ou un lieu fermé) ; lire la date dans un journal d'actions, qui ne couvre pas les retraits de l'auteur. -
Parcours déjà publié en cours de relecture. Un
needs_fixcorrigé par son auteur repassependinget sort du public jusqu'à la décision (choix 5 de la PR #420, à confirmer). Que voit celui qui l'ouvre d'un favori ? Recommandé : 404, comme un parcours introuvable, pour un état court et rare au MVP. Écarte : la 410 (elle annoncerait un retrait qui n'a pas eu lieu, avec une fausse date) ; un nouvel état « en cours de correction » (planche à faire). À reconsidérer : corriger unneeds_fixpar une révision, comme un parcours publié, pour qu'il reste visible avec son bandeau pendant la relecture (#53). -
Lien privé d'un parcours soumis. Recommandé : le jeton ouvre le parcours tant qu'il est
private,pendingouchanges_requestedsans avoir été publié, comme le dit « Partageable : oui » de Creation-et-Moderation › Cycle de vie ; l'auteur le lit sans jeton. Écarte : un jeton valable seulement enprivate, qui casserait les liens déjà envoyés dès que l'auteur soumet. -
L'auteur d'un parcours Premium. Recommandé : l'auteur a toujours droit à son propre parcours en entier, fiche et programmation, ce que
canAccessRoutene dit pas aujourd'hui (règle à étendre par #36 ou #66). Écarte : verrouiller l'auteur comme un autre non-abonné (il ne verrait plus ses étapes dans la fiche). -
Trajets exacts ou estimés, fiche et
/timeline. Recommandé : la fiche calcule durée, distance, marche, effort et sa timeline sur les trajets de la réponse (exacts quand Mapbox répond), pour que les heures s'additionnent avec les trajets affichés ; paliers et filtres restent sur l'enregistré./timelinereste sur nos estimations, celles que la programmation enregistre, ce qui amende la ligne actuelle « sur les trajets exacts de Mapbox » (D-088). Écarte :/timelineexacte (l'heure de fin vue dans E-06 différerait de quelques minutes de la sortie enregistrée, et chaque changement de date coûterait des requêtes Mapbox) ; une fiche entièrement estimée (« 15 min à pied » affiché, 13 min entre deux heures). -
Parcours Premium sans droit. Recommandé : aucun appel à Mapbox (trajets estimés, aucun tracé), conflits vides sur les étapes verrouillées,
/timelineen 402premium_required. Écarte : interroger Mapbox pour des étapes que l'appelant ne voit pas (coût sans usage) ; des conflits qui laisseraient deviner les horaires d'un lieu caché ; une timeline à un départ libre pour un parcours qu'il ne peut pas programmer. -
Cache et limite de la fiche. Recommandé :
no-storesur la réponse 200 deGET /routes/:id, et 60 requêtes par minute et par IP. Écarte : le cache public court de la convention « toutes les lectures publiques sont cachables » (il garderait les résultats Mapbox dans le CDN et l'app, D-088), qui devient une exception à écrire dans Conventions ; aucune limite (une boucle d'ouvertures épuiserait le quota Mapbox de 300 requêtes par minute pour tous). -
Le site
/r/:id(E-21). Son rendu est mis en cache 5 minutes, et sa carte du parcours ne montre jamais le détail des étapes (E-21, D-068) : il n'a besoin d'aucun trajet. Recommandé : le site lit la fiche avec?trips=estimated(nos estimations seules, aucun appel à Mapbox, réponse cachable 5 minutes comme/timeline). Écarte : un appel à Mapbox à chaque revalidation du site, pour des trajets qu'il n'affiche pas ; un résultat Mapbox gardé 5 minutes dans son cache (interdit par D-088) ; un endpoint de plus, propre au site. -
Avertissements dans la fiche. Recommandé : aucun
warningsnierrorsde cohérence dans la fiche ; le lecteur voitstatus, les conflits, la vérification et les dates de fin. Écarte : exposer les avertissements de l'éditeur (too_many_transitions,back_and_forth…), qui parlent au créateur et à la modération, pas à celui qui sort. - Budget. Recommandé : somme exacte, comme sur la card (D-066), l'arrondi aux 5 € restant dans l'app (une seule fonction partagée, que le test « 23 € donne 25 € » de #36 vise). Écarte : un budget déjà arrondi par l'API, que demande encore le corps de #36 et qui ferait deux règles entre la card et la fiche.
| Méthode | Route | Description |
|---|---|---|
| POST | /auth/email-link |
Sans jeton (D-090, #397, v0.4) : { email, language }, language valant fr ou en ; l'API génère le lien de connexion par l'Admin SDK Firebase (generateSignInWithEmailLink, adresse de retour et réglages du lien fixés par l'API, jamais par l'app) et l'envoie par Brevo au modèle « Connexion » de E-22, dans la langue demandée. 202 sans corps, identique que l'adresse ait un compte ou non ; 422 validation_error ; 429 rate_limited moins de 30 s après l'envoi précédent à la même adresse, au-delà de 5 envois par heure pour l'adresse ou de 30 par heure pour l'IP. Ni l'adresse ni le lien dans les journaux. Jusqu'à la v0.4, l'app demande le lien à Firebase directement |
| GET | /me |
Profil complet (bio, quartier, anecdotes, à la une), rôle, plan, préférences, canCreateRoute (faux pour un compte gratuit qui a déjà son parcours), certification (critères avec valeur, seuil et état). Plus de compteurs du mois (D-023) |
| PATCH | /me |
Prénom ou pseudo, avatar, bio (150 caractères), quartier (facultatif), isPublic, locale (fr, en, ou null pour la langue du téléphone, D-062), notificationPrefs ({ reminders, nextDay, myRoutes }, booléens, D-062), anecdotes (liste ordonnée [{ questionKey, answer }], 3 au plus, question unique, réponse de 150 caractères) et featuredRouteIds (3 au plus, parcours publiés de l'appelant), enregistrés en une transaction par « Enregistrer » de Modifier le profil ; 422 validation_error sinon |
| POST | /webhooks/revenuecat |
Webhook signé : met à jour plan et plan_expires_at
|
| DELETE | /me |
Suppression de compte, sans paramètre (D-025) : parcours publiés anonymisés (auteur « Membre Widoo »), tout le reste supprimé |
| POST | /me/devices |
Enregistrement du jeton push, avec la langue du téléphone (locale) : langue des push quand users.locale est nul (D-062) |
| POST | /me/email |
{ newEmail } (D-062) : envoie à la nouvelle adresse un lien de confirmation, dans la langue du compte ; l'ancienne adresse reste active. Remplace une demande en attente. 409 conflict (details.reason email_in_use), 422 validation_error, 429 rate_limited moins de 30 s après l'envoi précédent |
| POST | /me/email/confirm |
{ token } (lien ouvert dans l'app) : l'adresse change dans Firebase Auth (Admin updateUser, emailVerified) et dans users.email ; la connexion par e-mail suit, Apple et Google ne changent pas. 410 link_expired
|
| GET | /me/export |
Export de vos données (E-19 › Confidentialité, D-063) : un JSON (profil, anecdotes, à la une, favoris, sorties programmées et passées, notes, parcours créés avec leurs étapes et les liens de leurs photos, préférences), Content-Disposition: attachment; filename=widoo-donnees-AAAA-MM-JJ.json ; préparé à la demande, non conservé ; 429 rate_limited au-delà de quelques demandes par heure |
| DELETE | /me/email |
Annule le changement en attente (« Annuler » de E-19) ; le lien déjà envoyé ne marche plus |
🟡 Brouillon #383 du 2026-10-14, à valider par Ilan, voir Compte-et-Monetisation › Fin de la limite de création : proposé,
canCreateRouteest retiré deGET /me, sans remplaçant (jamais codé) : la création n'a plus de limite, la seule limite se dit à la soumission.
| Méthode | Route | Description |
|---|---|---|
| GET | /me/favorites |
|
| PUT / DELETE | /me/favorites/:routeId |
Sans limite (D-023) |
| GET | /me/planned |
Query status=
|
| POST | /me/planned |
{ routeId, startsAt } → programmation avec timeline et checklist. 402 premium_required (parcours Premium pour un non-abonné) ; sans limite sinon (D-023) |
| POST | /me/planned/:id/rating |
{ rating, comment? } ; exige un Go terminé ; crée ou met à jour route_ratings
|
| PATCH | /me/planned/:id |
startsAt, checklist, status (planned pour « Rétablir » ou « Reprogrammer », in_progress, completed, cancelled) ; changer startsAt recalcule la timeline et recale les rappels ; parcours Premium sans abonnement : 402 premium_required sur startsAt et in_progress (D-038). personalNote retiré (D-029) |
| POST | /me/planned/:id/steps/:stepId/done |
Coche une étape (Go) ; corps facultatif { signal: { kind, reason?, comment? } } pour la vérification sur place |
| GET | /me/planned/:id/maps-url |
URL Google Maps à pied jusqu'à une étape, construite côté serveur : une URL par étape, jamais pour tout le parcours (D-032) |
| Méthode | Route | Description |
|---|---|---|
| GET | /me/routes |
Mes parcours, tous statuts |
| POST | /routes |
Crée un brouillon, sans limite (révision du 2026-09-28 ; l'ancienne limite create_limit est abandonnée) |
| PATCH | /routes/:id |
Informations, ambiances, publics, conditions, transport |
| PUT | /routes/:id/steps |
Remplace la liste ordonnée des étapes ; retourne le résultat d'orchestration (computed, warnings, errors) |
| POST | /routes/:id/submit |
Passe en pending ; 422 coherence_blocked si règles bloquantes |
| POST | /routes/:id/status |
private, draft, unpublished par l'auteur |
| POST | /media/upload-url |
{ ownerType, ownerId, contentType } → URL signée et mediaId
|
| DELETE | /media/:id |
🟡 Brouillon #383 du 2026-10-14, à valider par Ilan, voir Compte-et-Monetisation › Fin de la limite de création : proposé,
POST /routes/:id/submitdocumente aussi la 429rate_limited(details: { reason: 'pending_routes', limit }) déjà codée (#66, #48) : au-delà desettings.max_pending_routes(3) parcours en relecture, une révision en attente comptant pour un, compteeditornon limité (à confirmer, question 4).
| Méthode | Route | Description |
|---|---|---|
| GET | /places/search |
Query q=, near=, category= : lieux Widoo existants |
| POST | /places |
Création ; répond 409 avec le candidat si un lieu proche de même nom existe, sauf force=true
|
| PATCH | /places/:id |
Auteur du lieu tant qu'il n'est utilisé que par ses parcours ; sinon modérateur |
| POST | /places/:id/signals |
`{ kind: 'confirm' |
| POST | /routes/:id/reports |
{ reason, stepId?, comment? } (D-056) ; stepId pour place_changed ; un signalement par utilisateur et par parcours sur 7 jours glissants (409 sinon) ; 403 sur ses propres parcours ; envoyé en différé par l'app hors connexion |
Rôle exigé par section : matrice de Admin-Web › Rôles. Toute écriture est journalisée dans admin_actions (D-085). Jeton sans second facteur : 403 mfa_required (D-087). Une décision sur un parcours ou un signalement déjà tranché par un autre membre de l'équipe : 409 already_decided avec la décision prise.
| Méthode | Route | Description |
|---|---|---|
| GET | /admin/moderation |
File pending et changes_requested
|
| GET | /admin/routes/:id |
Fiche de modération (A-02) : parcours tel qu'il sera publié, avertissements de cohérence, différence avec la version publiée pour une révision, chiffres et historique de l'auteur, historique du parcours, position dans la file (« 1 sur 14 ») |
| GET | /admin/counters |
Compteurs légers de la navigation (modération et ancienneté maximale, signalements, support, brouillons IA, traductions en échec), filtrés par rôle |
| POST | /admin/routes/:id/publish |
|
| POST | /admin/routes/:id/request-changes |
{ message } |
| POST | /admin/routes/:id/reject |
{ reasonCode, message } |
| POST | /admin/routes/:id/unpublish |
{ reasonCode, message } |
| GET | /admin/signals |
Groupés par lieu |
| POST | /admin/places/:id/verify |
Remet verified, purge les signaux ouverts |
| POST | /admin/places/:id/close |
{ reason } ; passe closed, parcours concernés en needs_fix
|
| POST | /admin/signals/:id/reject |
{ reason } |
| GET | /admin/reports |
Signalements de parcours ouverts, avec l'étape pour place_changed (D-056) |
| POST | /admin/reports/:id/resolve |
`{ outcome: 'accepted' |
| POST | /admin/ratings/:id/hide |
{ reason } ; masque un commentaire |
| POST | /admin/users/:id/certification |
`{ override: 'none' |
| GET | /admin/stats |
Compteurs du tableau de bord |
| POST | /admin/places/import |
CSV, rôle admin |
| POST | /admin/routes/:id/republish |
Republie un parcours dépublié |
| POST | /admin/routes/:id/reassign |
Réassigne à l'équipe Widoo |
| GET | /admin/ratings |
Avis avec commentaire, filtres note et parcours ; POST /admin/ratings/:id/unhide rétablit |
| PATCH | /admin/places/:id |
Édition d'un lieu, dont partner_booking_url et partner_program_id ; POST /admin/places crée un lieu |
| GET |
/admin/users, /admin/users/:id
|
Recherche et fiche (A-06) |
| PATCH | /admin/users/:id/role |
Rôle admin seulement ; jamais sur soi-même |
| POST |
/admin/users/:id/suspend, /reactivate
|
{ reason } ; un compte suspendu reçoit 403 account_suspended partout |
| POST | /admin/users/:id/gift |
{ until? , routeId? } : Premium offert jusqu'à une date ou un parcours offert, plan_source = gift
|
| GET | /admin/users/:id/export |
Export RGPD, même contenu que celui de l'app |
| DELETE | /admin/users/:id |
Même règle que la suppression dans l'app |
| POST, GET, PATCH, DELETE | /admin/ai-drafts |
Génération, liste, éditeur assigné, suppression des brouillons IA (A-07, #247) |
| GET | /admin/translations |
Traductions en échec ou corrigées ; POST /:id/retry, PATCH /:id (champ corrigé) |
| GET, POST, PATCH | /admin/affiliate-programs |
Programmes d'affiliation (A-09) |
| GET | /admin/bookable-steps |
Étapes réservables, origine du lien, clics sur 30 jours |
| GET, POST, PATCH | /admin/b2b-clients |
Partenaires B2B ; PUT /:id/routes : 3 à 10 parcours dans l'ordre (A-10) |
| GET | /admin/settings |
Réglages ; PATCH /admin/settings/:key (admin), validation par clé, effet sous une minute |
| GET | /admin/actions |
Journal (admin) |
| GET | /admin/support |
Messages « Nous écrire » ; POST /:id/reply (e-mail Brevo E-22), POST /:id/handled
|
Publics liés à l'admin : GET /go/:stepId enregistre un clic dans booking_clicks et redirige (302) vers le lien de réservation affiché (créateur d'abord, D-048) ; GET /public/partners/:slug sert la page partenaire B2B du site.
Appelées par Cloud Scheduler avec Authorization: Bearer <INTERNAL_TOKEN>, jamais par un utilisateur ni par l'admin web : 401 sans le bon jeton (comparé en temps constant) ; routes absentes quand le jeton n'est pas configuré ; Cache-Control: no-store ; hors de la documentation publique de l'API (D-066).
| Méthode | Route | Description |
|---|---|---|
| POST | /internal/recommendation |
Recalcul nocturne des composantes stables du score de recommandation dans routes.recommendation (qualité, fiabilité, fraîcheur, officiel, moments ; voir Filtres-et-Recherche › Recommandation) ; réponse { updated } (#63) |
| POST | /internal/certification |
Recalcul nocturne du badge Créateur certifié selon les seuils de settings
|
| POST | /internal/recompute |
Recalcul des parcours dont un lieu a changé |
| POST | /internal/reminders |
Rappels J-1 et H-1 des parcours programmés |
| POST | /internal/verification-decay |
Passage verified → stale après 90 jours |
Produit
- Vision-Produit
- Perimetre-MVP
- Personas-et-Parcours-Utilisateurs
- Ecrans
- Filtres-et-Recherche
- Orchestration
- Creation-et-Moderation
- Admin-Web
- Compte-et-Monetisation
- Direction-Artistique
Technique
- Modele-de-Donnees
- Architecture-Technique
- Site-Web
- API
- Securite-et-RGPD
- Conditions-d-utilisation
- Politique-de-confidentialite
- Analytics
Pilotage