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éthodeCheminCe qu’elle fait
POST/measuresCréer une mesure sans l’exécuter
GET/measuresLister vos mesures
GET/measures/{measureId}Lire une mesure
DELETE/measures/{measureId}Supprimer une mesure et ses médias
POST/measures/rppg/videoCréer et exécuter, depuis un fichier vidéo
POST/measures/rppg/imagesCréer et exécuter, depuis des images
POST/measures/{measureId}/rppg/videoExécuter une mesure existante depuis un fichier vidéo
POST/measures/{measureId}/rppg/imagesExé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.

ChampTypeBornes
externalIdchaîneJusqu’à 300 caractères. Votre propre identifiant pour la personne.
sex"M" | "F"
ageentier18 à 110
dateOfBirthchaîne de dateUne alternative à age ; les mêmes bornes s’appliquent
weightentier20 à 200, en kilogrammes
heightentier100 à 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é :

VariableA besoin de
hr, br, hrvRien — tirées du seul signal
bmiweight et height
bp, bpClass, healthScore, strsLes 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
PartieRemarques
videoJusqu’à 100 Mo. video/mp4, video/webm, video/x-msvideo, video/vnd.avi, video/quicktime, video/mov
userData[…]Parties nommées, une par champ
desiredVariablesSéparées par des virgules
tagsSé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.

EnvironnementBase de l'API
Productionhttps://api.saphere.ai
Testhttps://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
PartieRemarques
images3 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.

VariablesForme 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.