PharmAssistant API

PharmAssistant APIv1.0.0

Accès programmatique à l'assistant pharmaceutique PharmAssistant : chaque question suit le même moteur deux étapes que le chat (routage vers les bases documentaires spécialisées, exécution des outils, rédaction sourcée) et revient en JSON structuré, non streamé, sans aucun marqueur d'IA interne. Les clés du contrat sont en anglais, les contenus en français.

Base : https://api.pharmassistant.fr · Spécification machine : openapi.json

Authentification

Toutes les routes, sauf /v1/health, exigent une clé d'API au format pa_…, transmise au choix :

En-têteExemple
X-API-KeyX-API-Key: pa_votre_cle
AuthorizationAuthorization: Bearer pa_votre_cle

Les clés sont délivrées par l'équipe PharmAssistant (contact@pharmassistant.fr) : vous recevez un lien vers un portail sécurisé où vous générez votre clé vous-même et gérez sa péremption et ses plafonds. La génération de la clé requiert l'acceptation des conditions d'utilisation de l'API et de la politique de confidentialité. Ne transmettez jamais de données permettant d'identifier un patient dans vos requêtes.

Démarrage rapide

# Une question, une réponse structurée
curl "https://api.pharmassistant.fr/v1/ask" \
  -H "X-API-Key: pa_votre_cle" \
  -H "Content-Type: application/json" \
  -d '{"question": "Amoxicilline et allaitement ?"}'
# Ou en un seul GET, pour un test immédiat sans corps JSON
curl "https://api.pharmassistant.fr/v1/ask?q=amoxicilline+et+allaitement" \
  -H "X-API-Key: pa_votre_cle"
# Conversation côté serveur : "store": true à l'ouverture…
curl "https://api.pharmassistant.fr/v1/ask" \
  -H "X-API-Key: pa_votre_cle" -H "Content-Type: application/json" \
  -d '{"question": "Amoxicilline et allaitement ?", "store": true}'
# → la réponse porte "conversation_id": "conv_…"

# …puis conversation_id à chaque tour suivant (historique rechargé par le serveur)
curl "https://api.pharmassistant.fr/v1/ask" \
  -H "X-API-Key: pa_votre_cle" -H "Content-Type: application/json" \
  -d '{"question": "Et pendant la grossesse ?", "conversation_id": "conv_…"}'

Client officiel mono-fichier, aucune dépendance (bibliothèque standard), Python 3.8+ :

curl -O https://api.pharmassistant.fr/clients/pharmassistant.py
from pharmassistant import PharmAssistant

client = PharmAssistant("pa_votre_cle")   # ou variable PHARMASSISTANT_API_KEY

# Une question, une réponse structurée
reponse = client.ask("Amoxicilline et allaitement ?", store=True)
print(reponse["answer"])            # markdown propre
print(reponse["patient_advice"])    # discours prêt pour le patient

# Poursuivre la conversation : le serveur porte l'historique
suite = client.ask("Et pendant la grossesse ?",
                   conversation_id=reponse["conversation_id"])
print(suite["answer"])

Toute erreur de l'API lève PharmAssistantError (attributs status, code, message), avec les codes machine de la section Erreurs.

Client officiel mono-fichier, aucune dépendance (fetch natif), types complets du contrat inclus. Node.js 18+, Deno ou Bun, côté serveur (ne jamais embarquer la clé dans un navigateur) ; utilisable aussi en JavaScript après compilation :

curl -O https://api.pharmassistant.fr/clients/pharmassistant.ts
import { PharmAssistant } from "./pharmassistant";

const client = new PharmAssistant("pa_votre_cle");  // ou variable PHARMASSISTANT_API_KEY

// Une question, une réponse structurée (types complets du contrat)
const reponse = await client.ask("Amoxicilline et allaitement ?", { store: true });
console.log(reponse.answer);           // markdown propre
console.log(reponse.patient_advice);   // discours prêt pour le patient

// Poursuivre la conversation : le serveur porte l'historique
const suite = await client.ask("Et pendant la grossesse ?",
  { conversationId: reponse.conversation_id! });
console.log(suite.answer);

Toute erreur de l'API rejette PharmAssistantError (propriétés status, code, message), avec les codes machine de la section Erreurs.

Client officiel mono-fichier, aucune dépendance (java.net.http + parseur JSON embarqué), Java 11+ :

curl -O https://api.pharmassistant.fr/clients/PharmAssistant.java
PharmAssistant client = new PharmAssistant("pa_votre_cle");

// Une question, une réponse structurée
Map<String, Object> reponse = client.ask("Amoxicilline et allaitement ?",
    new PharmAssistant.AskOptions().store(true));
System.out.println(reponse.get("answer"));          // markdown propre
System.out.println(reponse.get("patient_advice"));  // discours prêt pour le patient

// Poursuivre la conversation : le serveur porte l'historique
Map<String, Object> suite = client.ask("Et pendant la grossesse ?",
    new PharmAssistant.AskOptions().conversationId((String) reponse.get("conversation_id")));
System.out.println(suite.get("answer"));

Les réponses sont des Map<String, Object> fidèles au JSON du contrat ; toute erreur lève PharmAssistant.ApiException (status, code), avec les codes machine de la section Erreurs.

Exemple de réponse (abrégée) :

{
  "id": "req_9f2c4a1b6d8e3f07",
  "object": "pharmassistant.answer",
  "created_at": "2026-07-23T14:05:12+00:00",
  "status": "ok",
  "mode": "standard",
  "question": "Amoxicilline et allaitement ?",
  "title": "Amoxicilline pendant l'allaitement",
  "answer": "L'amoxicilline est compatible avec l'allaitement…",
  "key_points": [
    {"polarity": "positive", "text": "Compatible avec l'allaitement (passage lacté très faible)."},
    {"polarity": "negative", "text": "Surveiller diarrhée ou éruption chez le nourrisson."}
  ],
  "patient_advice": "Vous pouvez poursuivre l'allaitement normalement…",
  "associated_sale": null,
  "clarification": null,
  "suggestions": ["Conduite à tenir en cas de candidose sous antibiotique ?"],
  "tools_used": ["lecrat_search", "lactmed_search"],
  "sources": {
    "lecrat_search": [{"title": "CRAT : amoxicilline", "url": "https://www.lecrat.fr/…"}],
    "lactmed_search": [{"title": "LactMed : Amoxicillin", "url": "https://www.ncbi.nlm.nih.gov/…"}]
  },
  "conversation_id": "conv_9a8b7c6d5e4f3a2b",
  "usage": {"elapsed_ms": 6480, "daily_requests_used": 12, "daily_requests_limit": 200}
}

Conversations

Trois façons de porter l'état d'un échange, au choix :

  • Sans état (défaut) : chaque ask est indépendant, aucune conversation n'est créée côté serveur.
  • Historique côté client : le champ history porte les tours précédents à chaque requête ; aucune conversation n'est créée côté serveur.
  • Conversation côté serveur : "store": true à l'ouverture crée une conversation et la réponse porte son conversation_id (conv_…). Les tours suivants n'envoient plus que question + conversation_id : l'historique est rechargé par le serveur avec les mêmes règles de reconstruction que le chat, la qualité des suivis est garantie par construction.

history est exclusif de conversation_id et de store (422 sinon). GET /v1/conversations liste les conversations de la clé, GET / DELETE /v1/conversations/{id} relit ou supprime.

Rétention : une conversation inactive depuis 90 jours est purgée automatiquement (RGPD) ; la suppression explicite est immédiate et définitive. Comme sur le chat, n'y stockez jamais de données identifiant un patient.

Journaux techniques : indépendamment de ces trois modes, le contenu des requêtes et des réponses est journalisé côté serveur pendant 30 jours (exploitation, sécurité, support), comme pour le chat. C'est une raison de plus de ne jamais transmettre de données identifiant un patient.

Streaming (SSE)

"stream": true (POST seulement) fait passer la réponse en Server-Sent Events (text/event-stream) : la réponse s'affiche au fil de la génération, et l'événement final done porte le même objet complet que la réponse non streamée (champs structurés, sources, usage).

curl -N "https://api.pharmassistant.fr/v1/ask" \
  -H "X-API-Key: pa_votre_cle" -H "Content-Type: application/json" \
  -d '{"question": "Amoxicilline et allaitement ?", "stream": true}'

event: start
data: {"id": "req_9f2c4a1b6d8e3f07", "mode": "standard", "conversation_id": null, …}

event: status
data: {"state": "tools_running", "tools": ["lecrat_search", "lactmed_search"]}

event: chunk
data: {"text": "L'amoxicilline est compatible avec l'allaitement…"}

event: done
data: { …le même objet complet que la réponse non streamée… }
ÉvénementContenu
startOuverture : id (= X-Request-Id), mode, question, conversation_id éventuel.
statusChamp state : routing (choix des bases), tools_running (+ liste tools, mêmes clés que tools_used), generating (rédaction).
chunkChamp text : fragment de la réponse markdown, déjà nettoyé (les blocs discours patient / vente associée et les points clés arrivent structurés dans done).
doneL'objet AskResponse complet, identique au mode non streamé. Toujours le dernier événement d'un flux réussi.
errorL'enveloppe d'erreur habituelle ; la requête est remboursée. Les erreurs de pré-vol (authentification, portées, quotas, conversation inconnue) restent des statuts HTTP classiques, avant tout événement.

Avec les clients officiels (Python, TypeScript, Java) :

for event, data in client.ask_stream("Amoxicilline et allaitement ?"):
    if event == "chunk":
        print(data["text"], end="", flush=True)
    elif event == "done":
        reponse = data   # le même JSON complet que client.ask()
for await (const { event, data } of client.askStream("Amoxicilline et allaitement ?")) {
  if (event === "chunk") process.stdout.write(data.text);
  else if (event === "done") reponse = data;   // le même JSON complet que client.ask()
}
Map<String, Object> reponse = client.askStream("Amoxicilline et allaitement ?",
    new PharmAssistant.AskOptions(),
    (event, data) -> { if (event.equals("chunk")) System.out.print(data.get("text")); });

Le streaming se combine librement avec les conversations (store / conversation_id) et le mode detailed.

Modes de réponse

  • standard : la réponse rapide du comptoir, sourcée et structurée (titre, réponse markdown, points clés à polarité, discours patient, vente associée, pistes d'approfondissement, sources complètes par base).
  • detailed : le moteur « Réponse détaillée » (analyse exhaustive, entité par entité), soumis à la portée ask:detailed de la clé et à son éventuel sous-plafond journalier.

Quotas et en-têtes

La facturation compte des requêtes, pas des tokens. Chaque réponse porte des en-têtes de pilotage :

En-têteContenu
X-RateLimit-LimitPlafond journalier de la clé (absent si la clé est illimitée).
X-RateLimit-RemainingRequêtes restantes aujourd'hui.
X-RateLimit-ResetHorodatage (epoch) du prochain minuit, heure de Paris.
X-Request-IdIdentifiant unique de la requête, à joindre à toute demande de support.
Retry-AfterSur un 429 rate_limited : secondes à attendre avant de réessayer.

GET /v1/me renvoie l'état complet de la clé : plafonds, portées, consommation du jour et du mois.

Endpoints

POST /v1/ask clé requise

Poser une question (JSON)

Forme nominale : JSON, seul question est obligatoire. Avec stream: true, la réponse passe en Server-Sent Events (text/event-stream) : événements start, status ({state: routing | tools_running | generating}), chunk ({text}), puis done (l'objet AskResponse complet, identique au mode non streamé) ou error (enveloppe d'erreur habituelle, quota remboursé). Les erreurs de pré-vol (auth, portée, quotas, conversation inconnue) restent des statuts HTTP classiques.

Corps de la requête

Objet AskRequest (JSON, voir le schéma).

Réponses

StatutDescription
200Réponse réussie. → AskResponse
401Clé d'API absente, invalide, révoquée ou expirée (expired_api_key). → ErrorResponse
403Pharmacie liée à la clé introuvable ou suspendue (pharmacy_not_found / pharmacy_suspended), ou portée manquante sur la clé (scope_denied). → ErrorResponse
404conversation_id inconnu pour cette clé (conversation_not_found : id invalide, conversation supprimée ou purgée après 90 jours d'inactivité). Requête non décomptée. → ErrorResponse
422Corps ou paramètres invalides (détail champ par champ dans error.details). → ErrorResponse
429Trop de requêtes : plafond journalier ou mensuel atteint (codes daily_limit_reached / monthly_limit_reached / detailed_limit_reached, reset à minuit ou au 1er du mois heure de Paris) ou rafale (code rate_limited, réessayer après l'en-tête Retry-After). Requête non décomptée. → ErrorResponse
500Erreur interne. Requête non décomptée (quota remboursé). → ErrorResponse
502Échec de génération ou réponse vide du moteur. Requête non décomptée (quota remboursé), réessayer. → ErrorResponse
GET /v1/ask clé requise

Poser une question (test rapide, paramètre q)

Forme minimale : GET /v1/ask?q=… (test rapide au curl/navigateur, sans corps JSON). Les paramètres optionnels reprennent ceux du POST : store=true ouvre une conversation côté serveur, conversation_id=conv_… la poursuit (l'historique client, lui, n'existe qu'en POST).

Paramètres de requête

ParamètreTypeDescription
qrequisstring
modestringdéfaut : "standard"
suggestionsbooleandéfaut : true
patient_advicebooleandéfaut : true
associated_salebooleandéfaut : true
instructionsstring
storebooleandéfaut : false
conversation_idstring

Réponses

StatutDescription
200Réponse réussie. → AskResponse
401Clé d'API absente, invalide, révoquée ou expirée (expired_api_key). → ErrorResponse
403Pharmacie liée à la clé introuvable ou suspendue (pharmacy_not_found / pharmacy_suspended), ou portée manquante sur la clé (scope_denied). → ErrorResponse
404conversation_id inconnu pour cette clé (conversation_not_found : id invalide, conversation supprimée ou purgée après 90 jours d'inactivité). Requête non décomptée. → ErrorResponse
422Corps ou paramètres invalides (détail champ par champ dans error.details). → ErrorResponse
429Trop de requêtes : plafond journalier ou mensuel atteint (codes daily_limit_reached / monthly_limit_reached / detailed_limit_reached, reset à minuit ou au 1er du mois heure de Paris) ou rafale (code rate_limited, réessayer après l'en-tête Retry-After). Requête non décomptée. → ErrorResponse
500Erreur interne. Requête non décomptée (quota remboursé). → ErrorResponse
502Échec de génération ou réponse vide du moteur. Requête non décomptée (quota remboursé), réessayer. → ErrorResponse
GET /v1/conversations clé requise

Lister mes conversations

Conversations gérées côté serveur de la clé (créées par un ask avec store=true), la plus récemment active d'abord. limit : 1 à 200 (défaut 50).

Paramètres de requête

ParamètreTypeDescription
limitintegerdéfaut : 50

Réponses

StatutDescription
200Réponse réussie. → ConversationListResponse
401Clé d'API absente, invalide ou révoquée. → ErrorResponse
422Validation Error → HTTPValidationError
GET /v1/conversations/{conversation_id} clé requise

Relire une conversation

Détail d'une conversation : titre, dates et tous les tours (user/assistant) dans l'ordre chronologique.

Paramètres de requête

ParamètreTypeDescription
conversation_idrequisstring

Réponses

StatutDescription
200Réponse réussie. → ConversationDetailResponse
401Clé d'API absente, invalide, révoquée ou expirée. → ErrorResponse
404Conversation inconnue pour cette clé (conversation_not_found). → ErrorResponse
422Validation Error → HTTPValidationError
DELETE /v1/conversations/{conversation_id} clé requise

Supprimer une conversation

Suppression immédiate et définitive (messages inclus). Sans appel explicite, une conversation est purgée automatiquement après 90 jours d'inactivité.

Paramètres de requête

ParamètreTypeDescription
conversation_idrequisstring

Réponses

StatutDescription
200Réponse réussie. → ConversationDeletedResponse
401Clé d'API absente, invalide, révoquée ou expirée. → ErrorResponse
404Conversation inconnue pour cette clé (conversation_not_found). → ErrorResponse
422Validation Error → HTTPValidationError
GET /v1/me clé requise

Ma clé et mes quotas

Réponses

StatutDescription
200Réponse réussie. → KeyInfoResponse
401Clé d'API absente, invalide ou révoquée. → ErrorResponse
GET /v1/health

État du service

Réponses

StatutDescription
200Réponse réussie. → HealthResponse

Erreurs

Toutes les erreurs partagent la même enveloppe {"error": {"code", "message", "details"}} avec un code machine stable. Une requête refusée ou échouée (429, 500, 502) n'est jamais décomptée.

StatutCodeQuand
401missing_api_keyAucune clé fournie dans les en-têtes.
401invalid_api_keyClé inconnue ou au mauvais format.
401revoked_api_keyClé révoquée.
401expired_api_keyClé arrivée à péremption (date configurée sur la clé).
403scope_deniedLa clé n'a pas la portée demandée (ex. ask:detailed).
403pharmacy_not_foundPharmacie liée à la clé introuvable.
403pharmacy_suspendedPharmacie suspendue.
404conversation_not_foundconversation_id inconnu pour cette clé (id invalide, conversation supprimée ou purgée après 90 jours d'inactivité). Requête non décomptée.
422invalid_requestCorps ou paramètres invalides (détail champ par champ dans error.details).
429rate_limitedRafale trop rapprochée : réessayer après l'en-tête Retry-After.
429daily_limit_reachedPlafond journalier de la clé atteint (reset à minuit, heure de Paris).
429monthly_limit_reachedPlafond mensuel atteint (reset au 1er du mois, heure de Paris).
429detailed_limit_reachedSous-plafond journalier du mode detailed atteint.
500internal_errorErreur interne du serveur.
502generation_failedÉchec de la génération par le moteur.
502empty_responseRéponse vide du moteur.

Schémas

Objets JSON du contrat, tels que renvoyés ou attendus par l'API.

AskRequest

ChampTypeDescription
questionrequisstringQuestion du pharmacien, en français.
max. 4000 car.
historyHistoryTurn[]Tours précédents de la conversation quand le CLIENT porte l'état (mode sans stockage). Exclusif de conversation_id et de store.
max. 24 éléments
conversation_idstring | nullContinuer une conversation gérée côté serveur (id conv_… renvoyé par un ask avec store=true) : l'historique est rechargé par le serveur et le nouvel échange y est ajouté. Exclusif de history.
max. 40 car.
storebooleantrue = créer une conversation côté serveur : l'échange est conservé (90 jours d'inactivité max) et la réponse porte son conversation_id, à renvoyer aux tours suivants. Inutile avec conversation_id (déjà stocké). Exclusif de history.
défaut : false
mode"standard" | "detailed"detailed = moteur Réponse détaillée (à froid).
défaut : "standard"
suggestionsbooleanAutoriser les pistes d'approfondissement (mode standard seulement).
défaut : true
patient_advicebooleanInclure le discours patient (bloc « Au comptoir »). False = le bloc est retiré et le champ reste null.
défaut : true
associated_salebooleanInclure le conseil de vente associée. False = le bloc est retiré et le champ reste null.
défaut : true
instructionsstring | nullInstructions libres additionnelles injectées dans le prompt de réponse.
max. 2000 car.
streambooleantrue (POST seulement) = réponse en Server-Sent Events (text/event-stream) : événements start, status, chunk, done, error. L'événement done porte le même objet AskResponse que la réponse non streamée. false = JSON classique.
défaut : false

HistoryTurn

ChampTypeDescription
rolerequis"user" | "assistant"
contentrequisstringmax. 16000 car.

AskResponse

ChampTypeDescription
idrequisstring
object"pharmassistant.answer"défaut : "pharmassistant.answer"
created_atrequisstring
statusrequis"ok" | "clarification_needed"
moderequis"standard" | "detailed"
questionrequisstring
titlestring | nullTitre court de l'échange (généré sur une question sans historique).
answerstring | nullRéponse en markdown standard, nettoyée des marqueurs internes et des blocs discours patient / vente associée (portés par leurs propres champs) ; les puces à polarité y sont réécrites en ✔/✘ lisibles. Null quand status = clarification_needed.
key_pointsKeyPoint[]Puces à polarité de la réponse en version structurée (positive = recommandé, negative = à éviter). Miroir des puces ✔/✘ de answer, pour exploitation machine.
patient_advicestring | nullDiscours prêt à tenir au patient (bloc « Au comptoir » du chat), séparé de la réponse. Null si absent ou désactivé par la requête.
associated_salestring | nullConseil de vente associée (bloc « Vente associée » du chat), séparé de la réponse. Null si absent ou désactivé par la requête.
clarificationClarification | nullPrésent quand un mot ambigu bloque le routage : reposer la question avec l'une des corrections proposées.
suggestionsstring[]Pistes d'approfondissement proposées.
tools_usedstring[]Bases documentaires interrogées pour cette réponse.
sourcesobject<string, SourceRef[]>Références COMPLÈTES par base interrogée (mêmes clés que tools_used, liste vide si la base n'a produit aucune référence).
conversation_idstring | nullIdentifiant de la conversation côté serveur (conv_…) quand la requête portait store=true ou conversation_id : à renvoyer tel quel au tour suivant pour poursuivre l'échange. Null en mode sans stockage.
usagerequisUsage

KeyPoint

Puce à polarité de la réponse : positive = recommandé, negative = à éviter.

ChampTypeDescription
polarityrequis"positive" | "negative"
textrequisstring

Clarification

ChampTypeDescription
wordrequisstring
correctionsrequisstring[]
messagerequisstring

SourceRef

ChampTypeDescription
titlerequisstring
urlstring | nullURL publique de la référence ; null pour une base locale sans lien.

Usage

ChampTypeDescription
elapsed_msrequisinteger
daily_requests_usedrequisinteger
daily_requests_limitinteger | nullPlafond journalier de la clé ; null = illimité.

ConversationListResponse

ChampTypeDescription
object"list"défaut : "list"
dataConversationSummary[]Conversations de la clé, la plus récemment active d'abord.

ConversationSummary

Entrée du listing des conversations gérées côté serveur.

ChampTypeDescription
idrequisstringIdentifiant public de la conversation (conv_…).
titlestring | nullTitre court, généré à l'ouverture.
message_countrequisintegerNombre de messages (tours user + assistant).
created_atstring | null
updated_atstring | nullDernière activité ; base de la purge automatique (90 jours d'inactivité).

ConversationDetailResponse

ChampTypeDescription
idrequisstring
object"pharmassistant.conversation"défaut : "pharmassistant.conversation"
titlestring | null
created_atstring | null
updated_atstring | null
messagesConversationMessage[]Tous les tours, dans l'ordre chronologique.

ConversationMessage

ChampTypeDescription
rolerequis"user" | "assistant"
contentrequisstring
created_atstring | null

ConversationDeletedResponse

ChampTypeDescription
idrequisstring
object"pharmassistant.conversation"défaut : "pharmassistant.conversation"
deletedtruedéfaut : true

KeyInfoResponse

ChampTypeDescription
idrequisstring
namerequisstring
key_hintrequisstring
pharmacy_slugrequisstring | null
daily_limitinteger | nullPlafond journalier ; null = illimité.
monthly_limitinteger | nullPlafond mensuel (mois calendaire Paris) ; null = illimité.
daily_detailed_limitinteger | nullSous-plafond journalier du mode detailed ; null = pas de sous-plafond.
limits_lockedbooleantrue = plafonds fixés par PharmAssistant, non modifiables depuis le portail client (contacter PharmAssistant pour les ajuster).
défaut : false
scopesstring[] | nullPortées accordées (ask, ask:detailed) ; null = toutes.
activerequisboolean
expires_atstring | nullPéremption de la clé ; null = sans expiration.
created_atrequisstring
last_used_atstring | null
daily_requests_usedrequisinteger
daily_detailed_usedintegerdéfaut : 0
month_requests_usedintegerdéfaut : 0

HealthResponse

ChampTypeDescription
status"ok"défaut : "ok"
servicestringdéfaut : "pharmassistant-api"
versionrequisstring

ErrorResponse

Enveloppe d'erreur unique de l'API, sur TOUS les statuts d'erreur.

ChampTypeDescription
errorrequisErrorInfo

ErrorInfo

ChampTypeDescription
coderequisstringCode d'erreur machine, stable (ex. daily_limit_reached).
messagerequisstringExplication lisible en français.
detailsany[] | nullPrésent sur les 422 : détail de validation champ par champ.

ValidationError

ChampTypeDescription
locrequisstring | integer[]
msgrequisstring
typerequisstring