Pages web
Votre logiciel tourne dans un navigateur, et la carte est dans le lecteur du poste où il s’affiche. Le composant navigateur d’Ophelix fait le lien en un clic, sans extension et sans rien installer de plus que l’agent.
La page ne parle jamais à l’agent, et l’agent n’ouvre aucun port. La page demande la lecture à Ophelix ; l’agent du poste la relève par une connexion sortante, dit dans quel lecteur se trouve la carte, et Ophelix lit. La page suit sa demande jusqu’au résultat.
Le code porteur ne passe jamais par la page : il se saisit dans la fenêtre de l’agent, sur le poste.
Une origine autorisée est un BrowserOrigin, bor_…, une session de page un BrowserSession, bss_…, et une demande à un poste un WorkstationAction, wsa_…. Droits de clé : workstations:read et workstations:write.
Je veux…
| Je veux… | Appel |
|---|---|
| Autoriser l’origine de ma page | POST /v1/browser-origins |
| Lister les origines autorisées | GET /v1/browser-origins |
| Retirer une origine | DELETE /v1/browser-origins/{id} |
| Donner une session à une page | POST /v1/browser-sessions |
| Savoir à quel poste une page s’est associée | GET /v1/browser-sessions/{id} |
| Lire une carte depuis la page | reader.readVitale(), avec le composant |
| Lire la CPS du praticien depuis la page | reader.readCps(), avec le composant |
| Vérifier sur mon serveur ce que la page a lu | GET /v1/workstation-actions/{id}, avec l’identifiant que la page envoie |
| Créer la facture du patient lu | POST /v1/claims depuis mon serveur, pour result.patient. Voir Facturer depuis la page |
| Faire signer une facture depuis la page | reader.secureClaim('clm_…'), avec le composant |
| Demander une lecture depuis mon serveur | POST /v1/workstation-actions |
| Suivre une demande | GET /v1/workstation-actions/{id} |
| Annuler une demande | POST /v1/workstation-actions/{id}/cancel |
Autoriser une origine
Une page ne pilote les postes que depuis une origine autorisée, dans le mode de la clé : schéma, hôte et port, comme l’en-tête Origin du navigateur. Ajoutez-la dans le tableau de bord, Paramètres, Lecteurs et cartes, ou par l’API :
curl
curl -X POST https://api.ophelix.fr/v1/browser-origins \
-H "Authorization: Bearer sk_test_…" \
-d origin=https://app.exemple.frUne adresse se réduit à son origine : https://App.exemple.fr:443/ devient https://app.exemple.fr. Un chemin, un joker ou un autre schéma sont refusés. HTTP n’est accepté que pour localhost, 127.0.0.1 et [::1], en mode test. Retirer une origine coupe ses sessions à leur requête suivante.
Ouvrir une session
La page ne voit jamais de clé d’API. Votre serveur, qui connaît son utilisateur, ouvre une session avec sa clé secrète et donne à la page client_secret :
curl
curl -X POST https://api.ophelix.fr/v1/browser-sessions \
-H "Authorization: Bearer sk_test_…" \
-d origin=https://app.exemple.fr{
"id": "bss_…",
"object": "browser_session",
"client_secret": "bss_…_secret_…",
"origin": "https://app.exemple.fr",
"workstation": null,
"linked_at": null,
"expires_at": 1793972454,
"created": 1793970654,
"livemode": false
}Le secret vaut trente minutes, pour cette seule origine, et ne se rend qu’à la création. Le composant en redemande un à votre serveur quand il expire. Si votre serveur sait déjà sur quel poste travaille son utilisateur, il le nomme par workstation : la session naît associée.
Associer la page au poste
Sans poste nommé, la page s’associe une fois par navigateur. Dans l’agent, Paramètres, « Associer une page web » montre un code de six caractères, valable cinq minutes et une fois. La personne le recopie dans la page. Le code prouve qu’elle est devant ce poste.
Le navigateur garde ensuite le souvenir de l’association, pour cette origine et ce mode : la page suivante retrouve son poste sans code. L’agent liste les pages associées et peut en retirer une ; la page redemande alors un code. Révoquer le poste les retire toutes.
Lire une carte
<script type="module">
import { OphelixBrowser } from '@ophelix/browser'
const reader = OphelixBrowser.create({
// Votre serveur ouvre la session avec sa clé secrète.
fetchClientSecret: async () => (await (await fetch('/ophelix/session', { method: 'POST' })).json()).client_secret,
})
const session = await reader.connect()
if (!session.workstation) {
await reader.link(codeAfficheParLAgent)
}
const action = await reader.readVitale()
// action.result.coverage et action.result.patient : relisez-les depuis votre serveur.
</script>readVitale() rend la demande une fois aboutie. Elle ne porte que des identifiants : result.coverage et result.patient pour une carte Vitale, result.practitioner pour une CPS, result.claim pour une signature. Votre serveur relit les droits par GET /v1/coverages/{id}, avec sa clé : aucune donnée de santé ne passe par le canal de la page.
Un échec lève OphelixBrowserError, avec code : un code d’erreur de l’API, comme workstation_offline, ou le failure_code de la demande. onStatus suit la demande, et signal l’annule tant que l’agent ne l’a pas prise.
Sans outil de build, le script autonome ophelix-browser.global.js pose window.OphelixBrowser : OphelixBrowser.create(…) s’écrit de même, et OphelixBrowser.OphelixBrowserError est la classe d’erreur.
Facturer depuis la page
La page lit les cartes et fait signer ; votre serveur crée la facture, avec sa clé. Entre les deux ne passent que des identifiants.
- Le praticien est celui de votre utilisateur. Sa CPS se lit une fois,
reader.readCps(), et votre serveur garderesult.practitioner. - La page lit la carte Vitale du patient et envoie à votre serveur l’identifiant de la demande,
wsa_…. - Votre serveur relit la demande, crée la facture pour
result.patient, puis la valide : elle passe àready. - La page fait signer la facture par la CPS du poste,
reader.secureClaim('clm_…'). Le code porteur se saisit dans la fenêtre de l’agent. Votre serveur relit la facture :secured.
Dans la page :
const reading = await reader.readVitale()
// Votre serveur crée la facture : la page ne lui nomme que la demande.
const response = await fetch('/claims', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: reading.id }),
})
const claim = await response.json()
// La CPS du poste signe. L'agent demande le code porteur dans sa propre fenêtre.
await reader.secureClaim(claim.id)Sur votre serveur, avec la bibliothèque Node :
import { Ophelix } from '@ophelix/node'
const ophelix = new Ophelix(process.env.OPHELIX_API_KEY!)
async function createClaim(actionId: string, practitioner: string) {
// La page ne fait que nommer la demande : on la relit, et on ne croit qu'une lecture aboutie,
// faite par une session que ce serveur a ouverte.
const reading = await ophelix.workstationActions.retrieve(actionId)
if (reading.type !== 'read_vitale' || reading.status !== 'succeeded' || !mySessions.has(reading.browser_session)) {
throw new Error('Demande inconnue')
}
const draft = await ophelix.claims.create(
{
practitioner,
patient: reading.result!.patient!,
third_party_amo: true,
context: { care_pathway: 'treating_doctor', common_law_accident: false },
lines: [{ nomenclature: 'ngap', code: 'ZC', service_date: '2026-11-06', unit_price_cents: 2650 }],
},
// Une clé par lecture : un double clic ne crée pas deux factures.
{ idempotencyKey: `facture-${reading.id}` },
)
return ophelix.claims.finalize(draft.id)
}mySessions est ce que votre serveur retient des sessions qu’il ouvre : l’id que rend POST /v1/browser-sessions, lié à son utilisateur. Une facture draft qui ne passe pas la validation rend claim_invalid au moment de finalize. Le reste de la vie de la facture est dans Factures.
Cycle de vie d’une demande
status | Sens |
|---|---|
pending | La demande attend que l’agent la relève |
in_progress | L’agent a trouvé la carte, Ophelix lit |
succeeded | Fini : result nomme ce qui a été lu ou signé |
failed | Fini : failure_code dit pourquoi |
canceled | Annulée avant que l’agent ne la prenne |
expired | Personne ne l’a prise en deux minutes |
Tant qu’une page est associée, l’agent relève les demandes toutes les deux secondes ; sinon, à son appel d’état suivant, au plus quinze secondes plus tard. Une demande prise va à son terme.
failure_code | Sens |
|---|---|
card_absent | Aucune carte du genre attendu dans les lecteurs du poste |
declined | La demande a été refusée sur le poste |
card_unreadable, card_blocked | La carte ne se lit pas, ou elle est bloquée |
engine_unavailable | Le moteur ne répond pas |
conflict | La facture à signer n’est pas ready |
En mode test
Le simulateur répond, quelle que soit la carte insérée : le scénario vitale_ok pour une carte Vitale, cps_doctor pour une CPS, ou celui que vous passez par scenario. L’agent d’un poste de test simule des cartes dans son écran Scénarios. Voir Cartes et scénarios de test.
{
"id": "wsa_…",
"object": "workstation_action",
"type": "read_vitale",
"status": "succeeded",
"workstation": "ws_…",
"browser_session": "bss_…",
"scenario": "vitale_exempted",
"claim": null,
"result": { "coverage": "cov_…", "patient": "pat_…" },
"failure_code": null,
"failure_message": null,
"expires_at": 1793970774,
"started_at": 1793970656,
"completed_at": 1793970656,
"created": 1793970654,
"livemode": false
}Événements
| Type | Quand |
|---|---|
workstation_action.succeeded | Une lecture ou une signature a abouti |
workstation_action.failed | Elle a échoué, failure_code dit pourquoi |
L’événement porte la demande entière. Une lecture de carte Vitale émet aussi coverage.retrieved, une signature claim.secured.
Pièges
- Un poste éteint refuse la demande : 409
workstation_offline, plutôt qu’une attente de deux minutes. - L’origine compte deux fois : elle doit être autorisée, et c’est celle de la session. Une page servie ailleurs reçoit
origin_not_allowed. - Test et production sont séparés : une page associée en test ne l’est pas en production, et un poste de test ne sert pas une session de production.
- Ne croyez pas un identifiant venu de la page. Le navigateur peut envoyer n’importe quel
wsa_…oupat_…. Relisez la demande avec votre clé :statusvautsucceeded,typeest celui attendu, etbrowser_sessionest une session que votre serveur a ouverte pour cet utilisateur. - Un double clic, une seule facture : passez une clé d’idempotence tirée de la demande, comme
facture-wsa_…. Le second appel rend la même facture. - Seule une facture
readyse signe : sansfinalize, la signature échoue avecconflict. - Le code porteur reste dans l’agent. Ne construisez aucun champ de saisie dans la page : la règle de la carte l’interdit.