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, puis ended. Vous pouvez la mener en un appel, ou 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/rppg, /measures/cppg | Lister seulement vos mesures faites depuis une caméra (rppg) ou depuis un capteur au contact (cppg) |
GET | /measures/{measureId} | Lire une mesure |
GET | /measures/{measureId}/rppg, /measures/{measureId}/cppg | Lire une mesure terminée, seulement si elle est de cette sorte |
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/rppg/captures | Créer et exécuter, depuis des captures brutes que vous avez recadrées vous-même |
POST | /measures/rppg/signal, /measures/cppg/signal | Créer et exécuter, depuis un signal déjà extrait |
POST | /measures/{measureId}/rppg/video, …/rppg/images, …/rppg/captures, …/rppg/signal, …/cppg/signal | Exécuter une mesure existante, depuis les mêmes sources |
GET | /external-users/{externalId} | Savoir si une personne que vous identifiez par externalId a des mesures, et un calibrage de tension |
Les routes « créer et exécuter » sont celles vers lesquelles se tourner. Elles répondent 201 Created ; exécuter une mesure existante répond 200 OK. Les deux rendent la mesure. 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 fonctionne le widget v1 : votre serveur crée la mesure, et le widget l’exécute. Une mesure ne s’exécute qu’une fois : en exécuter une déjà démarrée est refusé par un 400.
Cette page détaille les routes vidéo et images. Les corps des routes de captures et de signal sont dans la référence OpenAPI.
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, strs, physicalAge, faceBmi | Rien — tirées du seul visage |
bmi | weight et height |
bpClass | weight, height, age et sex |
healthScore | weight, height, age, sex et smokingStatus |
bp | externalId, et un calibrage pour cette personne |
bp se calibre personne par personne. Elle demande un externalId, et une mesure antérieure et complète de cette même personne pour laquelle une tension de référence a été saisie — seul le formulaire de fin de mesure du widget v1 en enregistre une aujourd’hui. Sans elle, bp revient avec NOT_CALIBRATED. GET /external-users/{externalId} répond { "externalId", "rppgCalibrated", "cppgCalibrated" }, ou 404 quand aucune de vos mesures ne porte cet identifiant.
Un champ manquant n'est pas une mesure ratée
Les variables qui n’ont pas pu être calculées reviennent avec un code d’erreur, le plus souvent un code nommant exactement ce qui manquait, tandis que le reste de la mesure se termine 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à.Deux autres champs facultatifs se tiennent à côté. tags est une liste d’étiquettes à vous, 1000 au plus, rendue avec la mesure et qui sert à filtrer la liste. computeOptions règle la façon dont une variable s’exprime : { "bpClass": { "scale": 2 } } rend bpClass sur deux niveaux — normal, ou non — au lieu de trois.
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, unwantedVariables | Séparées par des virgules, ou une partie par valeur |
tags | Séparés par des virgules, ou une partie par valeur |
computeOptions | L’objet, sérialisé en JSON |
`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 : la requête entière est refusée par un 400, et rien n’est mesuré. 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 vérifiez encore votre 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, 201 Created, 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. image/jpeg, image/jpg, image/png, image/bmp, image/x-ms-bmp, image/webp, image/heic |
Les autres parties sont celles de la route vidéo.
Images et vidéo ne calculent pas les mêmes variables
Une image fixe ne porte pas de pouls. Depuis des images, seule faceBmi est calculée — plus bmi, qui ne demande que weight et height. Toute variable lue sur le pouls revient avec MISSING_CAPTURES, et signal vaut null.
L’inverse vaut pour une vidéo : elle rend toutes les variables sauf faceBmi, qui lit des images fixes et revient avec MISSING_IMAGE.
Relire une mesure
GET /measures/{measureId}
Authorization: Bearer <apiKey>
Les mesures appartenant à un autre client rendent 404, exactement comme une mesure inexistante.
Lister ses mesures
GET /measures
Authorization: Bearer <apiKey>
Vous ne voyez jamais que les vôtres. Les mesures d’un autre client ne sont pas refusées : elles sont simplement absentes.
| Paramètre | Remarques |
|---|---|
take | De 1 à 1000. Vaut 100 par défaut. |
skip | 0 ou plus. Vaut 0 par défaut. |
filters | Restreint ce qui est rendu. Voir ci-dessous. |
La liste n’est pas triée, et son ordre n’est pas garanti d’un appel à l’autre : pour parcourir un grand ensemble page par page, réduisez-le par un filtre plutôt que de vous fier à skip seul. filters ne s’applique qu’à GET /measures — GET /measures/rppg et GET /measures/cppg l’acceptent sans l’appliquer.
Filtrer par étiquette
Un filtre est une condition — un champ, un opérateur, et les valeurs auxquelles le comparer :
{ "operator": "contains", "name": "tags", "values": ["cohort-a"] }
Il voyage en un seul paramètre de query, écrit sérialisé ou éclaté — les deux sont acceptés et disent la même chose :
GET /measures?filters={"operator":"contains","name":"tags","values":["cohort-a"]}
GET /measures?filters[operator]=contains&filters[name]=tags&filters[values][0]=cohort-a
Dans une vraie adresse, la forme sérialisée est encodée en pourcentage comme toute valeur de query ; curl -G --data-urlencode 'filters={…}' s’en charge pour vous.
tags porte plusieurs valeurs : une condition y compare donc une liste à une liste. C’est ce à quoi servent les trois opérateurs :
| Opérateur | Retient une mesure dont les étiquettes… | ["cohort-a", "pilot"] retient |
|---|---|---|
contains | portent toutes les valeurs listées | ["cohort-a"], ["cohort-a", "pilot"] |
overlaps | en portent au moins une | ["cohort-a"], ["pilot", "other"] |
contained-by | ne portent aucune étiquette hors de celles listées | ["cohort-a"], ["pilot"], [] |
| Champ | Valeurs acceptées |
|---|---|
operator | contains, overlaps, contained-by |
name | tags |
values | Une étiquette ou plusieurs |
Réunir plusieurs conditions
Pour en exiger plusieurs à la fois, réunissez-les par un and :
{ "operator": "and", "operands": [{ "operator": "contains", "name": "tags", "values": ["cohort-a"] }] }
and ne se justifie qu’à partir de deux conditions — une condition seule n’a besoin d’aucune enveloppe.
Nommez un champ une fois, et listez toutes ses valeurs
Un champ ne peut figurer que dans une condition : deux conditions surtags sont refusées par un 400. Ce n’est pas une limite à contourner — pour demander plusieurs étiquettes, listez-les dans values, que les opérateurs ci-dessus comparent.Ce qui est filtrable est une liste fermée
name accepte tags et rien d’autre. Un champ inconnu, un opérateur inconnu ou un and sans condition sont refusés par un 400 avant toute recherche — une faute de frappe revient donc en requête rejetée, jamais en page vide qu’on lirait comme « aucun résultat ».Supprimer une mesure
DELETE /measures/{measureId}
Authorization: Bearer <apiKey>
Cet appel retire les médias conservés — captures, images, vidéo et signaux — puis la mesure elle-même, et répond 200 OK avec un corps vide. Un objet absent n’est pas une erreur, de sorte qu’une suppression interrompue à mi-course se relance simplement. Une fois une suppression menée à son terme, la mesure n’existe plus, et un second appel répond 404.
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",
"createdAt": "2026-08-24T14:57:44.000Z",
"startedAt": "2026-08-24T14:57:46.000Z",
"endedAt": "2026-08-24T14:58:22.000Z",
"desiredVariables": null,
"unwantedVariables": null,
"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_EXTERNAL_ID" }
},
"userData": {
"externalId": null, "sex": "F", "age": 35,
"weight": 58, "height": 171, "smokingStatus": "non-smoker"
},
"tags": ["cohort-a"]
}
Une mesure qui n’est pas terminée s’arrête à son état : une mesure created ne porte ni startedAt ni rien de ce qui suit, une mesure started n’a ni endedAt, ni returnedVariables, ni signal, ni variables. userData vaut null quand rien n’a été fourni, et un champ que vous n’avez pas envoyé y vaut null ; une dateOfBirth revient sous la forme de l’age qu’elle donnait.
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 de ce que vous demandiez
Ce n’est pas un échec de traitement. Un compte sur lequel aucune variable n’a été activée termine toutes ses mesures sur une liste vide — même quand la capture s’est parfaitement déroulée.
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" } |
scale donne l’étendue de level : de 1 à 5 pour strs, de 1 à 3 pour bpClass — ou de 1 à 2 quand vous avez demandé deux niveaux.
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")
Codes d’erreur
Une erreur est un code sur lequel agir, non un message à afficher.
| Code | Ce qu’il dit |
|---|---|
CONFORMITY_FACE_PRESENCE, CONFORMITY_FACE_SIZE, CONFORMITY_INITIALISATION, CONFORMITY_LOW_FPS, CONFORMITY_MISSING_SIGNAL, CONFORMITY_MUCH_VARIATIONS, CONFORMITY_ONDULATIONS, CONFORMITY_POOR_LIGHT | La capture elle-même : le visage, sa taille, la cadence, la lumière, la stabilité du signal |
MISSING_USER_DATA, MISSING_USER_DATA_AGE, MISSING_USER_DATA_EXTERNAL_ID, MISSING_USER_DATA_HEIGHT, MISSING_USER_DATA_SEX, MISSING_USER_DATA_SMOKING_STATUS, MISSING_USER_DATA_WEIGHT | Un champ de userData dont la variable a besoin n’a pas été envoyé |
MISSING_CAPTURES, MISSING_IMAGE, MISSING_SIGNAL, EXCEEDING_MAX_IMAGE, INVALID_IMAGE_SIZE, INVALID_IMAGE_TYPE | La source envoyée ne permet pas cette variable |
MISSING_BP_CLASSIFICATION, MISSING_PROCESSOR, NOT_CALIBRATED | Il manque ce dont la variable dépend — pour NOT_CALIBRATED, le calibrage de la personne |
BAD_RMSSD, OUT_OF_RANGE, PROCESSOR_FAIL, UNKNOWN | Le calcul a refusé l’entrée, ou n’a pas abouti |
CREDIT_EXCEEDED | Votre compte n’a plus de crédit pour le mois ; la mesure n’est pas décomptée |
L’indicateur de qualité
"signal": { "qualityScore": { "value": 100, "error": null } }
signal vaut null quand la mesure n’avait aucun signal de pouls à évaluer — une mesure faite depuis des images, par exemple.
N'y bâtissez pas un seuil
qualityScore porte sur le signal lui-même, non sur la mesure dans son ensemble, et il ne juge pas à quel point cette mesure était exploitable. Une règle de la forme « rejeter en dessous de n » lui fait dire ce qu’il ne dit pas.Pour juger si une mesure est exploitable, lisez les variables une à une et leurs codes d’erreur : une variable qui n’a pas pu être calculée nomme elle-même le motif, qui est l’information qu’un seuil remplaçait.
Établissez ce jugement sur des données de Test d’abord. Les droits et les crédits se règlent par compte, si bien que ce que rend un compte de Test n’est pas nécessairement ce que rendra votre compte de Production. Environnements expose la séparation.