{"openapi":"3.0.3","info":{"title":"Astra API (TAQ)","version":"0.1.1","description":"API métier de Tutelle au quotidien (TAQ), appelée **par des serveurs** (jamais par le navigateur) :\nle moteur conversationnel (moteur simulé en DEV, documenté sur `/moteur-simule/docs` ; Datasulting ensuite),\nqui relaie le jeton de l'utilisateur émis par la webapp Astra, et la webapp elle-même si besoin.\nElle expose les données de Proxima **auxquelles l'utilisateur connecté a droit**.\nSocle repris de l'API d'Astra (contrat OpenAPI, clé d'API, jeton RS256, procédures stockées).\n\n## Principes\n- **L'appelant ne décide jamais des droits.** Chaque appel porte le jeton de l'utilisateur\n  (`Authorization: Bearer <JWT>`, émis et signé par la webapp) ; l'API en déduit l'utilisateur et sa structure\n  (client TAQ), et ne renvoie que les données autorisées. Aucun paramètre ne permet de choisir l'utilisateur ou le client.\n- Chaque appel porte aussi la **clé d'API** de l'appelant (`X-Api-Key`), propre à chaque environnement.\n- Réponses en JSON (UTF-8). Erreurs au format `application/problem+json` (RFC 7807) avec le code métier TAQ.\n- **0 résultat n'est pas une erreur** : réponse 200 avec une liste vide.\n- Chaque réponse renvoie l'en-tête `X-Request-Id` (celui reçu, ou un identifiant créé par l'API) :\n  le transmettre de bout en bout facilite le suivi d'un échange.\n\n## Démarrer\n1. Obtenir une **clé d'API** pour l'environnement (une par appelant, remise hors de ce document).\n2. Vérifier l'accès : `GET /v1/sante` avec l'en-tête `X-Api-Key` → `200` (`mode` indique `simule` ou `reel`).\n3. Pour une donnée métier, ajouter `Authorization: Bearer <jeton de l'utilisateur>` (jeton émis par la webapp Astra).\n4. En cas de problème, citer le `requestId` de la réponse (ou l'en-tête `X-Request-Id`).\n\n## Codes d'erreur\n| HTTP | `code` | Signification | Que faire |\n|---|---|---|---|\n| 400 | `PARAMETRE_INVALIDE` | paramètre inconnu, mal typé, hors bornes ou trop long | corriger la requête |\n| 400 | `94088` | aucun critère de recherche exploitable | demander un nom, un prénom, ou une relation avec un dossier |\n| 400 | `00006` | identifiant de dossier obligatoire | fournir `dossierId` |\n| 401 | `CLE_INVALIDE` | clé d'API absente, inconnue ou désactivée | vérifier l'en-tête `X-Api-Key` |\n| 401 | `JETON_INVALIDE` | jeton absent, invalide ou expiré (en-tête `WWW-Authenticate`) | demander un nouveau jeton à la webapp |\n| 403 | `00900` | pas d'accès au dossier, ou dossier inexistant (même réponse) | ne pas réessayer |\n| 404 | `INTROUVABLE` | route inconnue | voir ce document |\n| 405 | `METHODE_INTERDITE` | méthode non prévue (en-tête `Allow`) | utiliser la méthode indiquée |\n| 429 | `LIMITE_DEBIT` | trop de requêtes (clé, utilisateur, ou échecs répétés de l'adresse) | attendre `Retry-After` secondes |\n| 500 | `ERREUR_TECHNIQUE` | erreur côté TAQ | réessayer plus tard, signaler le `requestId` |\n| 503 | `NON_DISPONIBLE` | route pas encore branchée, ou environnement mal configuré | signaler à TAQ |\n\n## Mode simulé (DEV)\nTant qu'une route n'est pas branchée sur la base, l'environnement de DEV répond avec les **exemples de ce contrat**\n(données fictives). On peut demander une réponse d'erreur précise avec l'en-tête `Prefer: code=403`,\nou un exemple précis avec `Prefer: example=<nom de l'exemple>`. L'en-tête de réponse `X-Api-Mode: simule` l'indique.\n\n## Historique des versions\n- 0.1.1 (2026-10-12) : URL de la DEV, tableau des codes d'erreur, exemples `CLE_INVALIDE`, `PARAMETRE_INVALIDE` et `00006` ;\n  `/v1/sante` documente le seul refus possible (clé d'API) ; 503 `NON_DISPONIBLE`. Aucun changement de route ni de champ.\n- 0.1.0 (2026-10-12) : première version détachée d'Astra (recherche de personne, santé, clés publiques JWKS).\n","contact":{"name":"TAQ — équipe Proxima (MOE)"}},"servers":[{"url":"https://astra-api.dev-2023.tutelleauquotidien.fr","description":"DEV (derrière le proxy de la DEV ; appels internes au serveur sur http://127.0.0.1:8080 avec l'en-tête Host)"}],"tags":[{"name":"Personnes","description":"Recherche des personnes d'un dossier (majeur protégé, proches, professionnels)."},{"name":"Technique","description":"Disponibilité de l'API et clés publiques des jetons."}],"paths":{"/v1/personnes/recherche":{"get":{"tags":["Personnes"],"operationId":"rechercherPersonne","summary":"Rechercher une personne (majeur protégé ou correspondant) par son nom ou sa relation","description":"Trouve des personnes dans les dossiers **accessibles à l'utilisateur** : majeurs protégés (MP),\nproches (sœur, fils, conjoint…), professionnels (médecin, notaire, banque…).\nLa recherche par le nom est **phonétique** (tolère les fautes d'orthographe).\n\n**Choisir les paramètres selon la situation :**\n1. **Le dossier est connu** (`dossierId`) : nom connu → `nom` (+ `prenom`) ; seulement une relation\n   ou une profession (« sa sœur », « son médecin ») → `relation` seul.\n2. **Pas de dossier, on cherche un majeur protégé** → `rechercheMP=true` + `nom`.\n3. **Pas de dossier, on cherche quelqu'un qui n'est pas un majeur protégé** → `rechercheMP=false` + `nom`.\n\nSi l'on ne sait pas distinguer le nom du prénom, mettre `nomCertain=false` :\nl'API cherche aussi avec nom et prénom inversés (résultats marqués `modeRecherche = Inversee`).\nLe nombre de résultats est plafonné (50) ; `tronque` signale qu'il y en avait davantage.\n","security":[{"cleApi":[],"jetonUtilisateur":[]}],"parameters":[{"$ref":"#/components/parameters/RequestId"},{"name":"dossierId","in":"query","required":false,"description":"Identifiant du dossier, s'il est connu. Absent : recherche dans tous les dossiers accessibles (archivés exclus).","schema":{"type":"integer","format":"int32","minimum":1},"example":10476},{"name":"rechercheMP","in":"query","required":false,"description":"Sans dossier, `true` pour chercher un majeur protégé, `false` pour un correspondant. Ignoré avec `dossierId`.","schema":{"type":"boolean","default":false}},{"name":"nom","in":"query","required":false,"description":"Nom de famille, nom d'usage ou raison sociale.","schema":{"type":"string","minLength":1,"maxLength":100},"example":"Martin"},{"name":"prenom","in":"query","required":false,"description":"Prénom, s'il est connu.","schema":{"type":"string","minLength":1,"maxLength":100},"example":"Paul"},{"name":"nomCertain","in":"query","required":false,"description":"`false` si l'on ne sait pas lequel des deux mots est le nom : l'API cherche aussi avec les deux inversés.","schema":{"type":"boolean","default":true}},{"name":"relation","in":"query","required":false,"description":"Relation avec le majeur protégé (« soeur », « fils »…) ou profession (« medecin », « notaire »…).\nAvec `nom` : aide au classement. Sans `nom` et avec `dossierId` : seul critère.\n","schema":{"type":"string","minLength":2,"maxLength":100},"example":"soeur"}],"responses":{"200":{"description":"Recherche effectuée. La liste peut être vide : ce n'est pas une erreur.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResultatRecherchePersonne"},"examples":{"majeurProtege":{"summary":"Majeur protégé cherché par son nom","value":{"nbResultats":1,"tronque":false,"nbRelationsReconnues":0,"resultats":[{"dossierId":10476,"personneId":501234,"typeLien":"MP","nomUsage":null,"nom":"MARTIN","prenom":"Paul","raisonSociale":null,"relation":null,"profession":null,"relationCorrespond":false,"modeRecherche":"Directe","nbDossiers":1}]}},"relationDansDossier":{"summary":"« sa sœur », dossier connu","value":{"nbResultats":1,"tronque":false,"nbRelationsReconnues":1,"resultats":[{"dossierId":10476,"personneId":501240,"typeLien":"Proche","nomUsage":"DUBOIS","nom":"MARTIN","prenom":"Sophie","raisonSociale":null,"relation":{"code":"Soeur","libelle":"Sœur"},"profession":null,"relationCorrespond":true,"modeRecherche":"Relation","nbDossiers":1}]}},"aucunResultat":{"summary":"Aucune personne ne correspond","value":{"nbResultats":0,"tronque":false,"nbRelationsReconnues":0,"resultats":[]}}}}}},"400":{"$ref":"#/components/responses/ParametresInvalides"},"401":{"$ref":"#/components/responses/NonAuthentifie"},"403":{"$ref":"#/components/responses/AccesRefuse"},"429":{"$ref":"#/components/responses/TropDeRequetes"},"500":{"$ref":"#/components/responses/ErreurTechnique"},"503":{"$ref":"#/components/responses/NonDisponible"}}}},"/v1/sante":{"get":{"tags":["Technique"],"operationId":"verifierSante","summary":"Vérifier que l'API répond (test de connexion de l'appelant)","description":"Test technique de bout en bout : clé d'API valide, API disponible. Ne renvoie aucune donnée métier.\n","security":[{"cleApi":[]}],"parameters":[{"$ref":"#/components/parameters/RequestId"}],"responses":{"200":{"description":"API disponible.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Sante"},"examples":{"disponible":{"summary":"API disponible","value":{"statut":"ok","version":"0.1.1","mode":"simule","date":"2026-10-12T18:30:00+02:00"}}}}}},"401":{"$ref":"#/components/responses/CleInvalide"},"429":{"$ref":"#/components/responses/TropDeRequetes"},"500":{"$ref":"#/components/responses/ErreurTechnique"},"503":{"$ref":"#/components/responses/NonDisponible"}}}},"/.well-known/jwks.json":{"get":{"tags":["Technique"],"operationId":"lireClesPubliques","summary":"Clés publiques des jetons utilisateur (JWKS)","description":"Clés publiques (RS256) qui permettent de vérifier la signature des jetons utilisateur émis par la webapp.\nPublique, sans authentification.\n","security":[],"responses":{"200":{"description":"Jeu de clés publiques (RFC 7517).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Jwks"},"examples":{"cles":{"summary":"Une clé active (exemple)","value":{"keys":[{"kty":"RSA","use":"sig","alg":"RS256","kid":"webapp-dev-2026-10","n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw","e":"AQAB"}]}}}}}}}}}},"components":{"securitySchemes":{"cleApi":{"type":"apiKey","in":"header","name":"X-Api-Key","description":"Clé d'API de l'appelant, propre à chaque environnement (DEV, recette, PROD), communiquée hors de ce document.\nStockée hachée côté API, renouvelable.\n"},"jetonUtilisateur":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Jeton de l'utilisateur, émis et signé par la webapp (RS256, clés publiques : `/.well-known/jwks.json`), durée de vie courte (15 minutes).\nRevendications : `sub` (identifiant de l'utilisateur, en chaîne), `structure` (identifiant du client TAQ, entier), `iss`, `aud` (`astra-api`), `iat`, `nbf`, `exp`, `jti`.\nTolérance d'horloge : 30 secondes. Un jeton refusé donne 401 avec l'en-tête `WWW-Authenticate: Bearer error=\"invalid_token\"`.\n"}},"parameters":{"RequestId":{"name":"X-Request-Id","in":"header","required":false,"description":"Identifiant de l'échange, à transmettre de bout en bout (renvoyé tel quel dans la réponse).","schema":{"type":"string","maxLength":64,"pattern":"^[A-Za-z0-9._-]+$"},"example":"6f1c2e3a-0b7d-4f7e-9c44-2d6a1b0e9f12"}},"headers":{"RequestId":{"description":"Identifiant de l'échange (reçu, ou créé par l'API).","schema":{"type":"string"}}},"responses":{"ParametresInvalides":{"description":"Paramètres insuffisants ou incohérents. Codes : `PARAMETRE_INVALIDE` type, longueur ou paramètre inconnu ;\ncodes métier de la route (ex. `94088` aucun critère exploitable, `00006` identifiant de dossier obligatoire).\n","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Probleme"},"examples":{"parametreInvalide":{"summary":"Paramètre inconnu (l'utilisateur et le client viennent du jeton, jamais d'un paramètre)","value":{"type":"https://astra-api.example.invalid/erreurs/PARAMETRE_INVALIDE","title":"Paramètres invalides","status":400,"code":"PARAMETRE_INVALIDE","detail":"Paramètre inconnu : fkClient.","requestId":"6f1c2e3a-0b7d-4f7e-9c44-2d6a1b0e9f12"}},"dossierObligatoire":{"summary":"Identifiant de dossier obligatoire","value":{"type":"https://astra-api.example.invalid/erreurs/00006","title":"Paramètres invalides","status":400,"code":"00006","detail":"L'identifiant du dossier est obligatoire.","requestId":"6f1c2e3a-0b7d-4f7e-9c44-2d6a1b0e9f12"}},"aucunCritere":{"summary":"Aucun critère exploitable","value":{"type":"https://astra-api.example.invalid/erreurs/94088","title":"Recherche impossible","status":400,"code":"94088","detail":"Aucun critère de recherche exploitable (nom, prénom ou relation dans un dossier connu).","requestId":"6f1c2e3a-0b7d-4f7e-9c44-2d6a1b0e9f12"}}}}}},"NonAuthentifie":{"description":"Clé d'API absente ou invalide, ou jeton utilisateur absent, invalide ou expiré.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Probleme"},"examples":{"jetonExpire":{"summary":"Jeton utilisateur expiré","value":{"type":"https://astra-api.example.invalid/erreurs/JETON_INVALIDE","title":"Authentification requise","status":401,"code":"JETON_INVALIDE","detail":"Le jeton utilisateur est absent, invalide ou expiré.","requestId":"6f1c2e3a-0b7d-4f7e-9c44-2d6a1b0e9f12"}},"cleInvalide":{"$ref":"#/components/examples/CleInvalide"}}}}},"CleInvalide":{"description":"Clé d'API absente, inconnue ou désactivée.","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Probleme"},"examples":{"cleInvalide":{"$ref":"#/components/examples/CleInvalide"}}}}},"NonDisponible":{"description":"Route pas encore branchée sur la base en mode réel, ou environnement mal configuré\n(mode simulé demandé hors DEV).\n","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Probleme"},"examples":{"nonDisponible":{"summary":"Service non disponible","value":{"type":"https://astra-api.example.invalid/erreurs/NON_DISPONIBLE","title":"Service non disponible","status":503,"code":"NON_DISPONIBLE","detail":"Cette route n'est pas encore disponible en mode réel.","requestId":"6f1c2e3a-0b7d-4f7e-9c44-2d6a1b0e9f12"}}}}}},"AccesRefuse":{"description":"L'utilisateur n'a pas accès à cette ressource, ou elle n'existe pas : les deux cas renvoient\nla même réponse (403, code `00900`) pour ne pas révéler l'existence d'un dossier.\n","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Probleme"},"examples":{"dossierNonAccessible":{"summary":"Dossier non accessible","value":{"type":"https://astra-api.example.invalid/erreurs/00900","title":"Accès refusé","status":403,"code":"00900","detail":"Vous n'avez pas accès à ce dossier.","requestId":"6f1c2e3a-0b7d-4f7e-9c44-2d6a1b0e9f12"}}}}}},"TropDeRequetes":{"description":"Limite de débit atteinte ; réessayer après le délai indiqué par `Retry-After` (secondes).\nLimites par minute : par clé d'API, par utilisateur ; une adresse IP qui accumule les échecs\nd'authentification est aussi refusée jusqu'à la minute suivante.\n","headers":{"Retry-After":{"description":"Délai avant nouvel essai (secondes).","schema":{"type":"integer"}},"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Probleme"},"examples":{"limite":{"summary":"Trop de requêtes","value":{"type":"https://astra-api.example.invalid/erreurs/LIMITE_DEBIT","title":"Trop de requêtes","status":429,"code":"LIMITE_DEBIT","detail":"Trop de requêtes. Réessayez dans quelques secondes.","requestId":"6f1c2e3a-0b7d-4f7e-9c44-2d6a1b0e9f12"}}}}}},"ErreurTechnique":{"description":"Erreur technique côté TAQ ; le détail est dans le journal de l'API (retrouvé grâce à `requestId`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/RequestId"}},"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Probleme"},"examples":{"erreur":{"summary":"Erreur technique","value":{"type":"https://astra-api.example.invalid/erreurs/ERREUR_TECHNIQUE","title":"Erreur technique","status":500,"code":"ERREUR_TECHNIQUE","detail":"Une erreur technique est survenue. Réessayez plus tard.","requestId":"6f1c2e3a-0b7d-4f7e-9c44-2d6a1b0e9f12"}}}}}}},"examples":{"CleInvalide":{"summary":"Clé d'API absente ou invalide","value":{"type":"https://astra-api.example.invalid/erreurs/CLE_INVALIDE","title":"Authentification requise","status":401,"code":"CLE_INVALIDE","detail":"La clé d'API (en-tête X-Api-Key) est absente ou invalide.","requestId":"6f1c2e3a-0b7d-4f7e-9c44-2d6a1b0e9f12"}}},"schemas":{"ResultatRecherchePersonne":{"type":"object","required":["nbResultats","tronque","nbRelationsReconnues","resultats"],"properties":{"nbResultats":{"type":"integer","description":"Nombre total de personnes trouvées (avant la limite de 50)."},"tronque":{"type":"boolean","description":"`true` si plus de 50 personnes ont été trouvées."},"nbRelationsReconnues":{"type":"integer","description":"Nombre de relations ou professions reconnues à partir de `relation`."},"resultats":{"type":"array","maxItems":50,"items":{"$ref":"#/components/schemas/PersonneTrouvee"}}}},"PersonneTrouvee":{"type":"object","required":["dossierId","personneId","typeLien","nom","prenom","relationCorrespond","modeRecherche","nbDossiers"],"properties":{"dossierId":{"type":"integer","description":"Dossier où la personne a été trouvée ; `-1` si elle figure dans plusieurs dossiers."},"personneId":{"type":"integer"},"typeLien":{"type":"string","enum":["MP","Proche","Fournisseur","Autre"]},"nomUsage":{"type":"string","nullable":true},"nom":{"type":"string","nullable":true},"prenom":{"type":"string","nullable":true},"raisonSociale":{"type":"string","nullable":true},"relation":{"type":"object","nullable":true,"required":["code","libelle"],"properties":{"code":{"type":"string"},"libelle":{"type":"string"}}},"profession":{"type":"object","nullable":true,"required":["code","libelle"],"properties":{"code":{"type":"string"},"libelle":{"type":"string"}}},"relationCorrespond":{"type":"boolean"},"modeRecherche":{"type":"string","enum":["Directe","Inversee","Relation"]},"nbDossiers":{"type":"integer"}}},"Sante":{"type":"object","required":["statut","version","mode","date"],"properties":{"statut":{"type":"string","enum":["ok"]},"version":{"type":"string","description":"Version du contrat."},"mode":{"type":"string","enum":["simule","reel"]},"date":{"type":"string","format":"date-time"}}},"Jwks":{"type":"object","required":["keys"],"properties":{"keys":{"type":"array","items":{"type":"object","required":["kty","kid","n","e"],"properties":{"kty":{"type":"string","enum":["RSA"]},"use":{"type":"string","enum":["sig"]},"alg":{"type":"string","enum":["RS256"]},"kid":{"type":"string"},"n":{"type":"string"},"e":{"type":"string"}}}}}},"Probleme":{"type":"object","description":"Erreur au format RFC 7807, avec le code métier TAQ.","required":["type","title","status","code"],"properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"code":{"type":"string","description":"Code métier ou technique (`PARAMETRE_INVALIDE`, `CLE_INVALIDE`, `JETON_INVALIDE`, `LIMITE_DEBIT`, `ERREUR_TECHNIQUE`…)."},"detail":{"type":"string","description":"Message lisible, sans donnée personnelle."},"requestId":{"type":"string"}}}}}}