Prise en main

D’une page vide à une mesure aboutie, en une dizaine de minutes.

Ce guide parcourt l’intégration complète la plus courte : un point d’accès de jeton sur votre serveur, le widget dans une page, et un résultat dans votre console.

Il vous faut un compte client et sa clé d’API. Si vous n’en avez pas, 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 le même code contre un autre jeu d’hôtes — voir Environnements.

La clé d'API ne va jamais dans un navigateur

Votre clé d’API ouvre toutes les routes de l’API, n’expire pas, et ne se révoque qu’en désactivant votre compte client — 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

Le widget demande un jeton d’accès à votre application au début de chaque mesure. Votre serveur répond en appelant l’API avec votre clé.

Faites-le viser l’environnement auquel appartient votre clé d’API de Test :

EnvironnementBase de l'API
Productionhttps://api.saphere.ai
Testhttps://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: "websocket.measurement", 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-le 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": "websocket.measurement", "maxAttempts": 1 }],
  "expiresAt": "2026-08-24T15:12:44.000Z",
  "createdAt": "2026-08-24T14:57:44.000Z"
}

`value` n'est montré qu'une fois

Seul un condensé du jeton est conservé. Cette réponse est la seule occasion où l’API vous remettra value. 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 module ES servi par le CDN. Il se monte dans un div qui vous appartient et le remplit.

C’est le CDN depuis lequel vous le chargez qui choisit l’environnement — le widget en déduit seul l’api de mesure, si bien qu’un paquet servi par le CDN de Test parle au service de mesure de Test et jamais à la Production. Votre point d’accès à jetons ci-dessus est la seule partie qui ne suive pas d’elle-même : c’est vous qui le faites viser l’api correspondante.

EnvironnementURL du module
Productionhttps://cdn.saphere.ai/saphere-scan/v2/main.js
Testhttps://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 sur une origine non sécurisée, et il n’y a pas de contournement.

3. Lancer une mesure

Ouvrez la page. Vous devriez voir les écrans d’accueil, puis un écran de consentement, puis la caméra.

Installez-vous dans une lumière homogène, face à la caméra, et tenez-vous raisonnablement immobile pendant trente secondes. Regardez la console : le widget raconte tout le parcours.

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 aboutie. 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 soit une valeur avec error: null, soit value: null accompagné d’un code d’erreur qui nomme ce qui a échoué. Testez toujours error avant de lire value.

`returnedVariables` vide ?

Ce n’est pas un échec de traitement. Cela signifie qu’aucune variable n’a encore été accordée à votre compte client — l’état habituel d’un client de test tout neuf. Demandez à i-Virtual d’activer celles dont vous avez besoin.

À lire ensuite