Prise en main
D’une page vide à une mesure terminée, en une dizaine de minutes.
Cette page parcourt l’intégration complète la plus courte : un point d’entrée sur votre serveur qui délivre des jetons, le widget dans une page, et un résultat affiché dans la console de votre navigateur.
Il vous faut un compte et sa clé d’API. Si vous n’en avez pas encore, contactez i-Virtual pour un essai.
Tout ce qui suit se passe sur le Test, là où une première intégration a sa place. La Production, c’est la même intégration, à d’autres adresses. Voir Environnements.
La clé d'API ne va jamais dans un navigateur
Votre clé d’API ouvre toutes les routes de l’api et n’expire jamais. La seule façon de la révoquer est de désactiver votre compte, ce qui coupe toutes vos intégrations d’un coup. Elle a sa place sur votre serveur et nulle part ailleurs. Le widget est conçu pour n’avoir jamais besoin de la détenir.1. Monter un point d’accès de jeton
Au début de chaque mesure, le widget demande un jeton d’accès à votre application. Votre serveur répond en appelant l’api avec votre clé.
Faites-le viser l’environnement auquel appartient votre clé d’API.
| Environnement | Base de l'API |
|---|---|
| Production | https://api.saphere.ai |
| Test | https://api.test.saphere.ai |
// server.js — Node 20+, Express 4
import express from "express"
const app = express()
const API_BASE = "https://api.test.saphere.ai" // Production : https://api.saphere.ai
const API_KEY = process.env.SAPHERE_API_KEY // ne jamais l'écrire en dur
app.post("/api/saphere-token", async (request, response) => {
// Authentifiez VOTRE utilisateur ici d'abord. Ce point d'accès émet une
// habilitation : le laisser ouvert permet à quiconque de consommer votre
// quota de mesures.
const created = await fetch(`${API_BASE}/access-tokens`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`
},
body: JSON.stringify({
description: "web widget",
// maxAttempts: 1 rend le jeton à usage unique. L'omettre produirait
// un jeton rejouable jusqu'à son expiration.
permissions: [{ name: "measurement.scan", maxAttempts: 1 }],
duration: "15min"
})
})
if (!created.ok)
return response.status(502).json({ error: "could not mint a token" })
// Relayez la réponse entière : le widget y lit `value`, qui est le porteur.
// `id` et `expiresAt` n'ouvrent rien et se journalisent sans risque.
response.json(await created.json())
})
app.listen(3000)
Vérifiez qu’il fonctionne avant d’aller plus loin.
curl -sX POST http://localhost:3000/api/saphere-token | jq
{
"id": "8f1c2d34-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
"value": "ivat-XCq7…",
"description": "web widget",
"permissions": [{ "name": "measurement.scan", "maxAttempts": 1 }],
"expiresAt": "2026-08-24T15:12:44.000Z",
"createdAt": "2026-08-24T14:57:44.000Z"
}
`value` n'est montré qu'une fois
Seule une empreinte du jeton est conservée, jamais le jeton lui-même. Cette réponse est la seule et unique occasion où l’api vous remetvalue. Un jeton perdu ne se récupère pas. Il se révoque et se remplace.2. Poser le widget dans une page
Le widget est un seul fichier JavaScript servi par notre CDN. Il s’accroche à un div que vous fournissez, et le remplit.
L’adresse de CDN depuis laquelle vous le chargez choisit l’environnement. Le widget en déduit seul le service de mesure, si bien qu’un code servi par le CDN de Test parle au service de Test et jamais à la Production. Votre point d’entrée à jetons est la seule partie qui ne suive pas d’elle-même : c’est vous qui le faites viser l’api correspondante.
| Environnement | URL du module |
|---|---|
| Production | https://cdn.saphere.ai/saphere-scan/v2/main.js |
| Test | https://cdn.test.saphere.ai/saphere-scan/v2/main.js |
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Saphere Scan</title>
<style>
html, body { margin: 0; height: 100%; }
/* Le widget remplit son conteneur : il faut donc lui donner une hauteur. */
#scan { height: 100dvh; }
/* Sur un large écran, le garder dans une colonne au format téléphone. */
@media (min-width: 40rem) { #scan { aspect-ratio: 9 / 16; margin: 0 auto; } }
</style>
</head>
<body>
<div id="scan"></div>
<script type="module">
import SaphereScan from "https://cdn.test.saphere.ai/saphere-scan/v2/main.js"
const instance = SaphereScan.create("#scan", {
lang: "fr",
proxy: {
retrieveAccessToken: {
strategy: "delegate",
url: "/api/saphere-token"
}
},
onEvent: event => {
console.log(event.type, event)
if (event.type === "measure:result")
console.log("variables:", event.result.variables)
if (event.type === "measure:failed")
console.warn("measurement failed:", event.code)
}
})
await instance.bootstrap()
</script>
</body>
</html>
Servez cette page en HTTPS, ou depuis localhost. Les navigateurs refusent l’accès à la caméra depuis toute autre sorte d’adresse, et il n’y a pas de contournement.
3. Lancer une mesure
Ouvrez la page. Vous devriez voir les écrans d’introduction, puis un écran de consentement, puis la caméra.
Installez-vous dans une lumière homogène, face à la caméra, et restez raisonnablement immobile pendant trente secondes. Regardez la console : le widget rapporte chaque étape.
widget:ready
screen:enter { path: "/onboarding/0", name: "onboarding" }
detector:loading
detector:loaded
consent:accepted
camera:requesting
camera:ready { width: 640, height: 480, frameRate: 30 }
placement:started
placement:accepted
token:requesting { strategy: "delegate" }
token:retrieved
transport:connected
measure:start
measure:progress { percent: 1 }
…
measure:captures-sent { totalCaptures: 900, totalImages: 1 }
measure:result { result: { … } }
4. Lire le résultat
event.result est la mesure terminée. Ce que vous cherchez est variables.
{
"status": "ended",
"returnedVariables": ["hr", "br", "strs"],
"signal": { "qualityScore": { "value": 100, "error": null } },
"variables": {
"hr": { "value": { "mean": 72.4 }, "error": null },
"br": { "value": { "mean": 15.1 }, "error": null },
"strs": { "value": { "level": 2, "scale": { "min": 1, "max": 5 } }, "error": null }
}
}
Chaque variable est l’une de deux choses : une valeur avec error: null, ou value: null accompagné d’un code d’erreur qui dit ce qui a échoué. Testez toujours error avant de lire value.
`returnedVariables` vide ?
Ce n’est pas un échec. Cela veut dire qu’aucune variable n’a encore été accordée à votre compte, ce qui est l’état normal d’un compte de test tout neuf. Demandez à i-Virtual d’activer celles dont vous avez besoin.