Embed NeuroScan and receive results on your callback URL.
Request an API key from a Kalmo admin, then manage it at /settings after signing in.
GET /start?session_id=<your-id>&lang=es&features=heart_rate,hrv_finger,respiration_rate,blink_rate,plr&skip=saccade&age=34&gender=female&theme=dark&api_key=<raw-key>
es | en (default es)dark | lightOptional. Call this from your server before opening the iframe to attach a participant ID for the lab history and/or a skip list. The participant ID is turned into a pseudonymous code with a server-side secret and is never stored -- never put it in the iframe URL (URLs end up in browser history, logs and Referer headers).
POST /api/sessions
Authorization: Bearer nsk_...
{ "sessionId": "abc", "participantId": "12.345.678", "skip": ["pupil"] }
→ 201 { "runId": "…", "participant": "P-3F9A2C1B" }Authorization: Bearer nsk_...
Iframes cannot set headers. Pass api_key in the query; the server authorizes the session and redirects to /start without the key.
Grant camera and motion sensors to the iframe:
<iframe src="https://neuroscan.example/start?session_id=abc&lang=es&api_key=nsk_..." allow="camera *; microphone *; autoplay *; fullscreen *; accelerometer *; gyroscope *" style="width:100%;height:700px;border:0" />
The iframe posts messages to its parent: neuroscan:step-complete after each step attempt (a retried step arrives again with a higher step.attempt; the last one is kept), then neuroscan:complete or neuroscan:error. Optional hints: neuroscan:display (brightness max/restore -- the pupil flash needs a bright screen) and neuroscan:keep-awake. Always check event.origin.
window.addEventListener('message', (event) => {
if (event.origin !== 'https://neuroscan.example') return;
switch (event.data?.type) {
case 'neuroscan:step-complete': // { step: StepResult (step.attempt), index, total }
case 'neuroscan:complete': // { session_id, closedEarly }
case 'neuroscan:error': // { session_id, message, code? }
case 'neuroscan:display': // { brightness: 'max' | 'restore' } -- optional
case 'neuroscan:keep-awake': // { on: boolean } -- optional
}
});When the scan ends (or the person closes it early), NeuroScan POSTs to the callback_url configured on the API key. steps[] holds one result per step; metrics is the flattened union kept for older integrations.
{
"payloadVersion": 2,
"session_id": "abc",
"lang": "es",
"steps": [
{
"stepId": "contact_ppg",
"status": "completed",
"startedAt": "2026-09-25T12:00:20.000Z",
"durationMs": 64000,
"quality": { "score": 0.92, "motion": 0.92, "abstained": false },
"metrics": {
"heart_rate": { "value": 71, "source": "measured", "confidence": "high", "algoVersion": "finger-ppg-hr-v3", "method": "contact_ppg" },
"hrv_finger": { "value": 42, "source": "measured", "confidence": "high", "algoVersion": "finger-ppg-rmssd-v3" },
"respiration_rate": { "value": 14.5, "source": "measured", "confidence": "medium", "algoVersion": "ppg-riiv-rsa-v1" }
},
"attempt": 2,
"previousAttempts": [{ "attempt": 1, "status": "completed", "abstained": true, "reason": "no_finger" }],
"detail": { "sdnn": 51, "ibisMs": [843, 851], "diagnostics": { "channel": "g", "goodSec": 55, "reasons": {} } }
},
{ "stepId": "face", "status": "completed", "...": "blink_rate, facial_expression, expression_valence, gaze_stability, saccade_latency, saccade_accuracy" },
{ "stepId": "pupil", "status": "skipped", "...": "" },
{ "stepId": "self_report", "status": "completed", "metrics": { "physical_wellbeing": { "value": 7, "source": "declared", "confidence": "high", "algoVersion": "self-report-v1" } }, "detail": { "answers": {} } },
{ "stepId": "motion", "metrics": { "agitation": { "value": 0.12, "source": "computed", "confidence": "high", "algoVersion": "motion-agitation-v1" } } }
],
"indices": {
"algoVersion": "indices-v1",
"stressLevel": { "value": 44, "confidence": "medium", "calibration": "age_gender", "components": { "hrv": { "value": 42, "z": -0.4, "weight": 0.35 } } },
"autonomicBalance": { "value": 0.18, "confidence": "medium", "calibration": "age_gender", "components": {} }
},
"demographics": { "age": 34, "gender": "female" },
"closedEarly": false,
"metrics": { "...": "flattened union of every step's metrics + stress_level + autonomic_balance" },
"features_measured": ["hrv", "blink"],
"results": { "...": "v1 compatibility blob (hrv, rppg, blink, saccade, plr, questionnaire)" },
"timestamp": "2026-09-25T12:02:30.000Z",
"version": "3.3.0"
}Admins can try the full scan without session_id or API key at /playground (login required).
/playground