NeuroScan v3.3.0ES

Developer guide

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.

/start route

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>
  • session_id — required
  • lang — es | en (default es)
  • features — comma-separated. Accepts metric ids (heart_rate, hrv_finger, respiration_rate, blink_rate, gaze_stability, facial_expression, plr, saccade_latency, saccade_accuracy), step ids (contact_ppg, face, pupil) or the older feature ids (questionnaire, hrv, blink, saccade, pupiles). Setup and the final self-report always run unless skipped.
  • skip — optional, comma-separated steps or face sub-tasks to remove: contact_ppg, face, pupil, self_report, gaze, saccade. Setup can't be skipped.
  • age — optional, integer 13–110. Calibrates the indices against age-specific norms.
  • gender — optional, male | female | other. Calibrates the indices (other uses population norms).
  • theme — dark | light
  • api_key — query param for embeds (stripped after authorize). You can also use Authorization: Bearer on /api/embed/authorize.

Pre-register a session (server to server)

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

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.

Embed (iframe)

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"
/>

Live events (postMessage)

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
  }
});

Callback payload (v2)

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"
}

Sandbox

Admins can try the full scan without session_id or API key at /playground (login required).

/playground