Cette page décrit le protocole de communication bidirectionnel entre RisqueCV et ses partenaires.
RisqueCV.fr est un outil d’aide à la décision pour la prévention cardiovasculaire. Il permet d’évaluer le risque cardiovasculaire d’un patient et de générer un rapport PDF.
100% des calculs sont effectués côté client (Typescript), aucun envoi de données ni aucun calcul vers le serveur de RisqueCV.fr.
L’absence de serveur de calcul est un choix délibéré pour garantir la confidentialité des données des patients. Cette architecture rend impossible la mise à disposition d’une API REST ou SMART on FHIR.
L’intégration de RisqueCV.fr à un logicier métier repose donc sur l’API Standard window.postMessage, permettant un échange sécurisé des données cliniques. Ce protocole permet l’ouverture de RisqueCV.fr, le transfert de données cliniques du logiciel métier vers la fenêtre de RisqueCV.fr (sans transfert des données sur internet), la génération du PDF des résultats en local (librairie JS) et l’envoi du rapport PDF de RisqueCV.fr vers le logiciel métier sans transfert des données sur internet.
La méthode window.postMessage permet au médecin en consultation de préremplir les données cliniques et d’utiliser l’interface de RisqueCV.fr (courbes, graphiques, conseils, etc.) comme un outil d’aide à la décision.
L’intégration repose sur un protocole d’échange de messages asynchrones via window.postMessage :
RisqueCV est conçu pour être ouvert depuis votre application via :
window.parent) pour initier le dialogue.window.open('https://risquecv.fr/slug-partenaire/', '_blank'). Cela permet une communication bilatérale fluide via la référence window.opener.Le protocole suit une machine à états stricte pour garantir la sécurité des données :
message global.risquecv:ready pour vous informer que le tunnel de communication est ouvert et vous transmettre un sessionId unique.
sessionId reçu lors de l’étape de prefill.{ "type": "risquecv:ping" }. RisqueCV renverra immédiatement le signal ready.ready, vous envoyez vos données patient via un message risquecv:prefill.L’algorithme de RisqueCV ne se contente pas de recevoir vos données, il assure leur intégrité :
ack) : RisqueCV vous renvoie systématiquement un message risquecv:prefill:ack. Il contient la liste des clés acceptées et celles ignorées (clés inconnues ou valeurs hors-bornes).prefill.Lorsque le médecin a terminé son évaluation sur RisqueCV :
pdfmake (aucune donnée n’est envoyée à nos serveurs).risquecv:pdf.risquecv:error avec le code PDF_GENERATION_FAILED.Lors du clic sur le bouton de retour, RisqueCV émet un signal risquecv:close. Voir la section “Résilience” ci-dessous pour la gestion des Iframes.
Ce protocole a été conçu pour répondre aux exigences les plus strictes de confidentialité (RGPD / HDS) :
event.origin ne correspond pas strictement à un liste blanche de partenaires.Pour garantir une expérience sans friction, RisqueCV implémente plusieurs mécanismes de sécurité :
Si RisqueCV est ouvert en mode intégration mais ne reçoit aucun trafic valide (ni ping, ni prefill) dans les 5 secondes suivant son chargement, il bascule automatiquement en Mode Standard (autonome). Cela évite de bloquer le médecin si votre logiciel métier subit un bug technique.
Rediriger une Iframe vers une URL externe peut provoquer le chargement de votre propre portail à l’intérieur de l’Iframe (Effet Inception).
risquecv:close et reste sur sa page. C’est à votre application de capter ce message pour détruire l’Iframe (pour éviter l’inception)window.close()), et n’utilise sa redirection de secours (returnUrl) qu’en tout dernier recours.Pour que votre logiciel puisse intégrer RisqueCV dans une Iframe, le serveur de RisqueCV doit vous y autoriser (Content-Security-Policy: frame-ancestors). Si vous constatez une erreur “Clickjacking” ou “Refused to frame”, demandez à ce que votre domaine soit bien whitelisté dans nos entêtes.
Le payload peut contenir n’importe quelle combinaison des clés ci-dessous. Toutes les valeurs sont optionnelles (null ou omission pour ignorer). Toute clé inconnue sera ignorée.
PAS et non pas, HbA1c et non hba1c, age et non Age, etc.).mmol/L pour le cholestérol, % pour l'HbA1c).Ces booléens pilotent les 4 premières questions du formulaire.
Les mettre à false permet de sauter les questions de tri initiales.
| Clé | Type | Description | True si… |
|---|---|---|---|
atcd |
boolean |
Antécédents de maladie cardiovasculaire avérée. | Maladie coronaire (angor, IDM, SCA revascularisation), AVC, AIT, AOMI, anévrisme de l’aorte abdominale (voir la liste complète sur le site, sous le titre “Antécédents cardiovasculaires”) |
diabete |
boolean |
Présence d’un diabète (Type 1 ou 2). | Diabète type 1 ou 2 |
MRC |
boolean |
Maladie Rénale Chronique (DFG < 60 ou albuminurie). | DFG < 60 ou albuminurie ou “Maladie rénale chronique” |
autrepb |
boolean |
Autres situations (HyperCHO familiale, HTA secondaire, etc.). | Hypercholestérolémie familiale hétérozygote, grossesse, etc. (voir la liste complète sur le site, sous le titre “Autre situation particulière ?”) |
| Clé | Type | Unité | Description |
|---|---|---|---|
age |
number |
ans | Âge du patient |
PAS |
number |
mmHg | Pression Artérielle Systolique (mesurée ce jour si possible). |
CT |
number |
mmol/L | Cholestérol Total |
HDL |
number |
mmol/L | Cholestérol HDL |
LDL |
number |
mmol/L | Cholestérol LDL |
DFG |
number |
mL/min | Débit de Filtration Glomérulaire |
HbA1c |
number |
% | Hémoglobine glyquée |
crp |
number |
mg/L | Protéine C-réactive ultra-sensible (hs-CRP) (attention pas la CRP standard) |
imc |
number |
kg/m² | Indice de Masse Corporelle |
agediabete |
number |
ans | Âge lors du diagnostic du diabète |
age_atcd |
number |
ans | Âge lors du premier événement cardiovasculaire (AVC, AIT, SCA, IDM, AOMI, anévrisme de l’aorte abdominale) |
| Clé | Type | Valeurs autorisées (exemples) |
|---|---|---|
sexe |
string |
"homme", "femme" |
typediabete |
string |
"type1", "type2", "autre" |
albuminurie |
string |
"non", "micro", "oui" |
pays |
string |
"France", "region_low", "region_moderate", "region_high", "region_veryhigh" |
Note sur la région/pays : Nous recommandons d’utiliser directement les codes de stratification européenne du risque (
"region_low","region_moderate","region_high","region_veryhigh") si votre logiciel en dispose. À défaut, vous pouvez envoyer le nom du pays européen en toutes lettres (ex:"France","Belgique"), RisqueCV déduira automatiquement la région associée.
| Clé | Type | Description |
|---|---|---|
tabac |
boolean |
Tabagisme actif actuel. |
aspirine |
boolean |
Traitement antiagrégant plaquettaire en cours. |
insuline |
boolean |
Diabète traité par insuline. |
hyperCHOfamille |
boolean |
Hypercholestérolémie familiale hétérozygote connue. |
retinopathie |
boolean |
Présence d’une rétinopathie diabétique. |
neuropathie |
boolean |
Présence d’une neuropathie diabétique. |
atcd_coronarien |
boolean |
Antécédent de maladie coronaire (IDM, SCA, revascularisation). |
atcd_cerebrovasculaire |
boolean |
Antécédent d’AVC ou d’AIT. |
atcd_aomi |
boolean |
Antécédent d’Artériopathie Oblitérante des Membres Inférieurs. |
atcd_anevrismeAorte |
boolean |
Antécédent d’anévrisme de l’aorte abdominale. |
microangiopathie3sites |
boolean |
Présence d’une microangiopathie sur ≥3 sites (ex: rétino + neuro + albu). |
autreFacteurMajeur |
boolean |
Présence d’un autre facteur de risque majeur (pour l’HF). |
evaluationComplete |
boolean |
Force l’affichage de l’évaluation complète (tunnel détaillé). |
Voici des exemples prêts à l’emploi selon la méthode de rendu que vous utilisez (WebView ou Popup). N’hésitez pas à les dérouler pour voir le code.
// --- 1. CONFIGURATION ---
const CONFIG = {
version: 1, // Version du protocole d'intégration RisqueCV
partnerSlug: 'votre-slug', // Identifiant de votre logiciel (ex : "weda")
targetOrigin: 'https://risquecv.fr' // Origine stricte
};
// --- 2. ÉTAT DE LA SESSION ---
const webview = document.getElementById('risquecv-webview');
let handshakeTimeout = null;
// --- 3. FONCTIONS UTILITAIRES ---
function fermerWebView() {
webview.style.display = 'none';
webview.src = 'about:blank';
}
/** Transmet le PDF (Base64) à votre API pour l'enregistrer dans le dossier patient */
async function sauvegarderPdf(base64Data, filename) {
try {
console.log(`Enregistrement du PDF ${filename} en cours...`);
// Exemple d'appel API interne à votre logiciel
const response = await fetch('/api/votre-logiciel/patients/12345/documents', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
nom_fichier: filename,
contenu_base64: base64Data,
type_document: 'EVALUATION_RISQUE_CV'
})
});
if (response.ok) {
console.log("✅ PDF sauvegardé avec succès dans le dossier patient.");
} else {
console.error("❌ Échec de la sauvegarde côté serveur.");
}
} catch (error) {
console.error("Erreur technique lors de la sauvegarde du PDF :", error);
}
}
// --- 4. DÉCLENCHEMENT DE L'OUVERTURE ---
document.getElementById('bouton-ouvrir-risquecv').addEventListener('click', () => {
// Charger l'URL d'intégration
webview.src = `${CONFIG.targetOrigin}/${CONFIG.partnerSlug}/`;
webview.style.display = 'block';
// Sécurité : Timeout si le chargement échoue. On effectue un "Ping" pro-actif.
handshakeTimeout = setTimeout(() => {
console.warn("⏳ Délai d'attente dépassé : RisqueCV ne répond pas. Tentative de Ping...");
// L'envoi dépend de votre framework (ici webview standard HTML5)
if (webview.contentWindow) {
webview.contentWindow.postMessage({ type: 'risquecv:ping' }, CONFIG.targetOrigin);
}
}, 3000);
});
// --- 5. GESTION DES MESSAGES ---
// L'écouteur dépend de votre pont natif. window.addEventListener (Electron), window.chrome.webview.addEventListener (WebView2), etc.
window.addEventListener('message', (event) => {
// Ignorer tout message ne provenant pas de RisqueCV
if (event.origin !== CONFIG.targetOrigin) return;
const msg = event.data;
switch (msg.type) {
// Si on reçoit "ready", alors on peut envoyer les données du patient
case 'risquecv:ready':
clearTimeout(handshakeTimeout);
// Envoi des données cliniques du patient
const payloadMessage = {
type: 'risquecv:prefill',
version: CONFIG.version,
partner: msg.partner,
sessionId: msg.sessionId,
payload: {
// Les données doivent être formatées avant l'envoi (ex: "femme" et pas "Femme" ni "F")
age: 55,
sexe: "femme",
tabac: false,
PAS: 142,
CT: 5.2,
HDL: 1.3
// Ajoutez vos autres variables cliniques ici. Le payload n'a pas besoin d'être exhaustif.
}
};
webview.contentWindow.postMessage(payloadMessage, CONFIG.targetOrigin);
break;
// Si on reçoit "ack", alors RisqueCV a bien reçu les données
case 'risquecv:prefill:ack':
// Si nécessaire pour debug, on peut vérifier l'injection
if (msg.status === 'partial') {
console.warn('⚠️ Données ignorées par RisqueCV (hors-bornes ou inconnues) :', msg.ignoredKeys);
}
break;
// Si on reçoit "pdf", alors on peut sauvegarder le PDF recu dans le dossier du patient
case 'risquecv:pdf':
fermerWebView();
sauvegarderPdf(msg.data, msg.filename);
break;
// Si le médecin clique sur le bouton "Retour au logiciel", on masque l'interface
case 'risquecv:close':
console.log("Fermeture demandée par RisqueCV.");
fermerWebView();
break;
// Si on reçoit "error", il y a eu un problème métier ou protocolaire
case 'risquecv:error':
console.error(`❌ Erreur RisqueCV [${msg.code}]:`, msg.message);
break;
}
});
// --- 1. CONFIGURATION ---
const CONFIG = {
version: 1, // Version du protocole d'intégration RisqueCV
partnerSlug: 'votre-slug', // Identifiant de votre logiciel (ex : "weda"))
targetOrigin: 'https://risquecv.fr' // Origine stricte
};
// --- 2. ÉTAT DE LA SESSION ---
let popupWindow = null;
let messageListener = null;
let pollClosedInterval = null;
let handshakeTimeout = null;
// --- 3. FONCTIONS UTILITAIRES ---
/** Nettoie les écouteurs pour éviter les fuites de mémoire (memory leaks) */
function cleanupSession() {
if (messageListener) {
window.removeEventListener('message', messageListener);
messageListener = null;
}
if (pollClosedInterval) {
clearInterval(pollClosedInterval);
pollClosedInterval = null;
}
if (handshakeTimeout) {
clearTimeout(handshakeTimeout);
handshakeTimeout = null;
}
popupWindow = null;
}
/** Transmet le PDF (Base64) à votre API pour l'enregistrer dans le dossier patient */
async function sauvegarderPdfEnBaseDeDonnees(base64Data, filename) {
try {
const response = await fetch('/api/votre-logiciel/patients/12345/documents', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
nom_fichier: filename,
contenu_base64: base64Data,
type_document: 'EVALUATION_RISQUE_CV'
})
});
if (response.ok) {
console.log("✅ PDF sauvegardé avec succès dans le dossier du patient.");
} else {
console.error("❌ Échec de la sauvegarde du PDF côté serveur.");
}
} catch (error) {
console.error("Erreur réseau lors de la sauvegarde du PDF :", error);
}
}
// --- 4. DÉCLENCHEMENT DE L'OUVERTURE ---
document.getElementById('bouton-ouvrir-risquecv').addEventListener('click', () => {
// Anti-spam : ramener la fenêtre au premier plan si elle est déjà ouverte (ne pas relancer le window.open pour éviter de recharger la page et de casser le Handshake)
if (popupWindow && !popupWindow.closed) {
popupWindow.focus();
return;
}
// Nettoyage complet avant ouverture
cleanupSession();
// Ouverture de l'interface RisqueCV dans un nouvel onglet
popupWindow = window.open(`${CONFIG.targetOrigin}/${CONFIG.partnerSlug}/`, '_blank');
if (!popupWindow) {
alert("Ouverture bloquée. Veuillez autoriser les pop-ups pour utiliser RisqueCV.");
return;
}
// Détection de la fermeture manuelle par l'utilisateur (croix de la fenêtre)
pollClosedInterval = setInterval(() => {
if (popupWindow && popupWindow.closed) {
console.info("ℹ️ Session RisqueCV terminée (fenêtre fermée par l'utilisateur).");
cleanupSession();
}
}, 1000);
// Timeout si le site distant est inaccessible. On effectue un "Ping" pro-actif.
handshakeTimeout = setTimeout(() => {
console.warn("⏳ Délai d'attente dépassé : RisqueCV ne répond pas. Tentative de Ping...");
if (popupWindow && !popupWindow.closed) {
popupWindow.postMessage({ type: 'risquecv:ping' }, CONFIG.targetOrigin);
}
}, 3000);
// --- 5. GESTION DES MESSAGES ---
messageListener = (event) => {
// Ignorer tout message ne provenant pas de RisqueCV
if (event.origin !== CONFIG.targetOrigin) return;
const msg = event.data;
switch (msg.type) {
// Si on reçoit "ready", alors on peut envoyer les données du patient
case 'risquecv:ready':
clearTimeout(handshakeTimeout);
// Envoi des données cliniques du patient
popupWindow.postMessage({
type: 'risquecv:prefill',
version: CONFIG.version,
partner: msg.partner,
sessionId: msg.sessionId,
payload: {
// Les données doivent être formatées avant l'envoi (ex: "femme" et pas "Femme" ni "F")
age: 55,
sexe: "femme",
tabac: false,
PAS: 142,
CT: 5.2,
HDL: 1.3
// Ajoutez vos autres variables cliniques ici. Le payload n'a pas besoin d'être exhaustif.
}
}, CONFIG.targetOrigin);
break;
// Si on reçoit "ack", alors RisqueCV a bien reçu les données
case 'risquecv:prefill:ack':
// Si nécessaire pour debug, on peut vérifier l'injection
if (msg.status === 'partial') {
console.warn('⚠️ Données ignorées par RisqueCV (hors-bornes ou inconnues) :', msg.ignoredKeys);
}
break;
// Si on reçoit "pdf", alors on peut sauvegarder le PDF recu dans le dossier du patient
case 'risquecv:pdf':
// Fermeture propre de la popup
if (popupWindow) popupWindow.close();
cleanupSession();
// Enregistrement du PDF dans le dossier du patient (Base64)
sauvegarderPdfEnBaseDeDonnees(msg.data, msg.filename);
break;
// Si le médecin clique sur le bouton "Retour au logiciel"
case 'risquecv:close':
console.log("Fermeture demandée par RisqueCV.");
if (popupWindow) popupWindow.close();
cleanupSession();
break;
// En cas d'erreur métier ou protocolaire
case 'risquecv:error':
console.error(`❌ Erreur RisqueCV [${msg.code}]:`, msg.message);
break;
}
};
window.addEventListener('message', messageListener);
});
Tous les messages partagent une structure de base : type, version (fixée à 1) et partner.
risquecv:ready (RisqueCV ➡️ Votre Logiciel)Envoyé dès que RisqueCV est prêt à recevoir des données. Ce message contient le sessionId requis pour la suite.
{
"type": "risquecv:ready",
"version": 1, // Il s'agit de la version du protocole d'intégration RisqueCV
"partner": "votre-slug", // ex : "weda", "doctolib", "medistory", etc.
"sessionId": "uuid-genere-par-risquecv", // Identifiant unique de la session
"capabilities": {
"prefill": true, // Indique que RisqueCV accepte le pré-remplissage
"pdfReturn": "base64" // Indique que RisqueCV peut renvoyer le PDF en base64
}
}
risquecv:ping (Votre Logiciel ➡️ RisqueCV)Permet de solliciter le renvoi du signal ready.
{
"type": "risquecv:ping"
}
risquecv:prefill (Votre Logiciel ➡️ RisqueCV)Message de réponse au ready. Permet d’injecter le contexte patient.
{
"type": "risquecv:prefill",
"version": 1,
"partner": "votre-slug",
"sessionId": "L_ID_RECU_DANS_READY",
"payload": {
"age": 55,
"sexe": "femme",
"tabac": false,
"PAS": 142, // pour les valeurs numériques, il faut envoyer des nombres (pas d'unités)
"atcd": true,
// ... voir dictionnaire des clés ci-dessus
}
}
risquecv:prefill:ack (RisqueCV ➡️ Votre Logiciel)Envoyé après réception du prefill. Confirme quelles données ont été validées et injectées.
{
"type": "risquecv:prefill:ack", // acknowledge
"version": 1,
"partner": "votre-slug",
"sessionId": "L_ID_RECU_DANS_READY",
"status": "ok", // "ok" ou "partial"
"acceptedKeys": ["age", "sexe", "tabac", "atcd"],
"ignoredKeys": ["cleInconnue"]
}
risquecv:pdf (RisqueCV ➡️ Votre Logiciel)Envoyé lorsque le médecin clique sur le bouton de transfert dans RisqueCV. Contient le document final.
{
"type": "risquecv:pdf",
"version": 1,
"partner": "votre-slug",
"sessionId": "L_ID_RECU_DANS_READY",
"filename": "RisqueCV_NomPatient.pdf",
"mimeType": "application/pdf",
"encoding": "base64",
"data": "JVBERi..." // Flux binaire converti en string Base64 (environ 50kB)
}
Note technique : La propriété
datacontient la chaîne Base64 brute du PDF (ex:JVBERi0...). Elle ne contient pas l’en-tête Data URI. Si vous souhaitez générer un lien de téléchargement ou l’afficher, vous devez concaténer la chaîne ainsi en Javascript :const pdfUrl = "data:application/pdf;base64," + msg.data;
risquecv:close (RisqueCV ➡️ Votre Logiciel)Envoyé lorsque l’utilisateur clique sur le bouton de retour. Indique que l’hôte doit fermer l’interface d’intégration.
{
"type": "risquecv:close",
"version": 1,
"partner": "votre-slug",
"sessionId": "L_ID_RECU_DANS_READY"
}
risquecv:error (RisqueCV ➡️ Votre Logiciel)Envoyé en cas de rupture du protocole ou d’erreur critique de session.
{
"type": "risquecv:error",
"version": 1,
"partner": "votre-slug",
"sessionId": "L_ID_RECU_DANS_READY", // Optionnel selon le type d'erreur
"code": "INVALID_SESSION",
"message": "La session d integration ne correspond pas a la page ouverte."
}
| Code | Signification |
|---|---|
INVALID_MESSAGE_FORMAT |
Le JSON ne respecte pas la structure de base. |
UNSUPPORTED_VERSION |
La version du protocole est différente de celle utilisée par le backend de RisqueCV. |
UNSUPPORTED_MESSAGE_TYPE |
Le type de message n’est pas pris en charge. |
INVALID_SESSION |
Le sessionId est manquant ou incorrect. |
PAYLOAD_NOT_A_FLAT_OBJECT |
Le payload n’est pas un objet JSON plat. |
VALID_BUT_EMPTY_PAYLOAD |
Aucune donnée clinique valide n’a été trouvée dans le payload (objet vide ou toutes les clés sont inconnues). |
INVALID_PARTNER |
Le slug partenaire ne correspond pas à l’URL ouverte. |
PDF_GENERATION_FAILED |
La génération du PDF a échoué. |
PREFILL_ALREADY_APPLIED |
Un prefill a déjà été appliqué pour cette session. |
Si vous rencontrez des difficultés (pas de réponse au handshake, données non injectées, etc.), vous pouvez activer le Mode Verbeux de RisqueCV.
Ajoutez simplement le paramètre debug_integration=1 à l’URL :
https://risquecv.fr/votre-slug/?debug_integration=1[RisqueCV] Initialisation intégration pour partenaire : votre-slug[RisqueCV] Origine non autorisée : https://votre-url-test.com (Si votre domaine n’est pas dans notre liste blanche).[RisqueCV] Source de message non attendue. (Si le message ne provient pas de l’objet window ayant ouvert RisqueCV).INVALID_SESSION, UNSUPPORTED_VERSION, etc.) est journalisé avec une explication textuelle détaillée.event.data.