Mesures
Créer des mesures, envoyer une vidéo ou des images enregistrées, et relire le résultat.
Une mesure traverse trois états — created, started, ended — et l’API vous laisse la mener soit en un appel, soit en deux.
Les routes
| Méthode | Chemin | Ce qu’elle fait |
|---|---|---|
POST | /measures | Créer une mesure sans l’exécuter |
GET | /measures | Lister vos mesures |
GET | /measures/{measureId} | Lire une mesure |
DELETE | /measures/{measureId} | Supprimer une mesure et ses médias |
POST | /measures/rppg/video | Créer et exécuter, depuis un fichier vidéo |
POST | /measures/rppg/images | Créer et exécuter, depuis des images |
POST | /measures/{measureId}/rppg/video | Exécuter une mesure existante depuis un fichier vidéo |
POST | /measures/{measureId}/rppg/images | Exécuter une mesure existante depuis des images |
Les routes « créer et exécuter » sont celles vers lesquelles se tourner. La forme en deux temps existe pour les cas où il vous faut l’identifiant avant d’avoir le média — c’est ainsi que le widget s’en sert.
Décrire la personne
Plusieurs variables ne se calculent pas à partir du seul signal. userData est l’endroit où vous dites ce qu’il faut.
| Champ | Type | Bornes |
|---|---|---|
externalId | chaîne | Jusqu’à 300 caractères. Votre propre identifiant pour la personne. |
sex | "M" | "F" | |
age | entier | 18 à 110 |
dateOfBirth | chaîne de date | Une alternative à age ; les mêmes bornes s’appliquent |
weight | entier | 20 à 200, en kilogrammes |
height | entier | 100 à 250, en centimètres |
smokingStatus | "smoker" | "non-smoker" |
Tous les champs sont facultatifs. Ce que vous omettez ne fait que réduire ce qui peut être calculé :
| Variable | A besoin de |
|---|---|
hr, br, hrv | Rien — tirées du seul signal |
bmi | weight et height |
bp, bpClass, healthScore, strs | Les champs démographiques ; celui qui manque est signalé variable par variable |
Un champ manquant n'est pas une mesure ratée
Les variables qui n’ont pas pu être calculées reviennent porteuses d’un code d’erreur — le plus souvent un code nommant exactement ce qui manquait — tandis que le reste de la mesure aboutit normalement. Vous ne perdez pas la fréquence cardiaque parce que vous ignoriez la taille de la personne.Choisir les variables
Deux tableaux facultatifs réduisent ce qui est calculé :
{
"desiredVariables": ["hr", "br"],
"unwantedVariables": ["faceBmi"]
}
Omettez les deux et la mesure calcule tout ce à quoi votre compte a droit.
Ils réduisent ; ils n'accordent pas
Demander une variable à laquelle votre compte n’a pas droit ne l’active pas. Le droit se configure sur le compte client, et ces champs ne peuvent que réduire ce que ce droit permet déjà.Envoyer une vidéo enregistrée
POST /measures/rppg/video
Authorization: Bearer <apiKey>
Content-Type: multipart/form-data
| Partie | Remarques |
|---|---|
video | Jusqu’à 100 Mo. video/mp4, video/webm, video/x-msvideo, video/vnd.avi, video/quicktime, video/mov |
userData[…] | Parties nommées, une par champ |
desiredVariables | Séparées par des virgules |
tags | Séparés par des virgules |
`userData` voyage en parties nommées
Écrivez userData[weight], userData[height], userData[age] — chacun comme sa propre partie multipart.
Une partie userData unique contenant du JSON sérialisé n’est pas acceptée sur ces routes, et c’est la cause la plus fréquente d’une mesure qui ne rend mystérieusement aucun bmi. C’est la forme en parties nommées qui fonctionne.
L’appel ci-dessous s’adresse à la Production. Envoyez-le au Test tant que vous validez encore une chaîne — les mesures qui y sont faites n’apparaissent jamais dans vos données de Production.
| Environnement | Base de l'API |
|---|---|
| Production | https://api.saphere.ai |
| Test | https://api.test.saphere.ai |
curl -sX POST https://api.saphere.ai/measures/rppg/video \
-H "Authorization: Bearer $SAPHERE_API_KEY" \
-F "video=@./capture.mp4;type=video/mp4" \
-F "userData[sex]=F" \
-F "userData[age]=35" \
-F "userData[weight]=58" \
-F "userData[height]=171" \
-F "userData[smokingStatus]=non-smoker" \
-F "tags=cohort-a,pilot"
La réponse est la mesure aboutie — la même forme que celle livrée par le widget.
Envoyer des images
POST /measures/rppg/images
| Partie | Remarques |
|---|---|
images | 3 fichiers au plus. Chacun entre 4 Kio et 30 Mo. |
Tout le reste est identique à la route vidéo.
Relire une mesure
GET /measures/{measureId}
Authorization: Bearer <apiKey>
Les mesures appartenant à un autre client rendent 404, en texte brut, exactement comme une mesure inexistante.
Supprimer une mesure
DELETE /measures/{measureId}
Authorization: Bearer <apiKey>
Cet appel retire les médias conservés — captures, images, vidéo, signaux — puis la ligne de la mesure. Il est rejouable sans risque : un objet absent n’est pas une erreur, de sorte qu’une suppression interrompue à mi-course se relance simplement.
C'est l'appel qui tient une promesse de confidentialité
Si votre produit dit aux utilisateurs qu’un scan ne laisse rien derrière lui, c’est cet appel qui le rend vrai. Rien d’autre ne retire les médias.Le résultat
{
"id": "3f1a5c88-0b2d-4e6a-9c11-7d5e8a2b4f60",
"status": "ended",
"type": "rppg",
"createdAt": "2026-08-24T14:57:44.000Z",
"startedAt": "2026-08-24T14:57:46.000Z",
"endedAt": "2026-08-24T14:58:22.000Z",
"returnedVariables": ["hr", "br", "strs", "bp"],
"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 },
"bp": { "value": null, "error": "MISSING_USER_DATA_AGE" }
},
"userData": { "sex": "F", "weight": 58, "height": 171 },
"tags": ["cohort-a"]
}
returnedVariables est un droit, non un résultat
C’est le champ le plus souvent mal lu. Il énumère les variables que votre compte client a le droit de recevoir, réduites par les desiredVariables / unwantedVariables que vous avez passés. Il n’énumère pas ce qui a réussi.
`returnedVariables: []` signifie que le compte n'accorde rien
Ce n’est pas un échec de traitement. Un client fraîchement créé n’a aucune variable activée, de sorte que toutes ses mesures se terminent sur une liste vide — même quand le signal était parfait et que qualityScore est revenu à 100.
La signature à reconnaître : un signal présent, et des variables vides. Regardez le compte, pas la capture.
Formes des valeurs
Chaque variable est l’une de deux choses : une valeur avec error: null, ou value: null accompagné d’un code d’erreur.
| Variables | Forme en cas de succès |
|---|---|
hr, br, hrv | { "value": { "mean": 72.4 }, "error": null } |
strs, bpClass | { "value": { "level": 2, "scale": { "min": 1, "max": 5 } }, "error": null } |
bp | { "value": { "systole": 118, "diastole": 76 }, "error": null } |
physicalAge, bmi, faceBmi, healthScore | { "value": 24.1, "error": null } |
| n’importe laquelle | { "value": null, "error": "CONFORMITY_POOR_LIGHT" } |
Testez toujours error en premier :
const hr = result.variables.hr
if (hr?.error === null)
console.log("heart rate:", hr.value.mean)
else if (hr)
console.warn("heart rate unavailable:", hr.error)
else
console.warn("heart rate not granted to this account")
L’indicateur de qualité
"signal": { "qualityScore": { "value": 100, "error": null } }
Il est binaire
qualityScore vaut 100 ou 0, et il répond à exactement une question : le signal a-t-il livré au moins une fenêtre exploitable pour la calibration tensionnelle ?
Ce n’est pas une note sur cent, et il ne juge pas à quel point la mesure était exploitable. Une règle de seuil bâtie dessus n’a que deux issues possibles, ce qui n’est certainement pas ce que vous vouliez.
Pour juger si une mesure est exploitable, lisez les variables une à une et leurs codes d’erreur.
Construisez ce jugement sur des données de Test d’abord : les droits et le quota se règlent par compte, donc ce que rend un client de Test n’est pas nécessairement ce que rendra votre client de Production. Environnements expose la séparation.