Repository navigation
API
Statut : 🟡 Brouillon · Màj : 2026-10-08 · Tickets : #164 #306
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). - 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, 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é. 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 |
| GET | /routes/:id/timeline |
Query startsAt= : timeline recalculée et conflits |
| 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).
| Méthode | Route | Description |
|---|---|---|
| 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 |
| 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 |
| 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
- Mentions-legales
- Analytics
Pilotage