NeuroScan v3.3.0EN

Guía para desarrolladores

Embebé NeuroScan y recibí los resultados en tu callback URL.

Pedí una API key a un admin de Kalmo y administrala en /settings después de iniciar sesión.

Ruta /start

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 — requerido
  • lang — es | en (por defecto es)
  • features — separado por comas. Acepta ids de métricas (heart_rate, hrv_finger, respiration_rate, blink_rate, gaze_stability, facial_expression, plr, saccade_latency, saccade_accuracy), ids de pasos (contact_ppg, face, pupil) o los ids de features anteriores (questionnaire, hrv, blink, saccade, pupiles). La preparación y la autoevaluación final siempre se ejecutan, salvo que se omitan.
  • skip — opcional, pasos o sub-tareas del rostro a quitar, separados por comas: contact_ppg, face, pupil, self_report, gaze, saccade. La preparación no se puede omitir.
  • age — opcional, entero 13–110. Calibra los índices con normas por edad.
  • gender — opcional, male | female | other. Calibra los índices (other usa normas poblacionales).
  • theme — dark | light
  • api_key — query param para embeds (se elimina tras autorizar). También podés usar Authorization: Bearer en /api/embed/authorize.

Pre-registrar una sesión (servidor a servidor)

Opcional. Llamalo desde tu servidor antes de abrir el iframe para asociar un ID de participante al historial y/o una lista de pasos a omitir. El ID se convierte en un código seudónimo con un secreto del servidor y nunca se guarda: nunca lo pongas en la URL del iframe (las URLs quedan en el historial del navegador, logs y encabezados Referer).

POST /api/sessions
Authorization: Bearer nsk_...
{ "sessionId": "abc", "participantId": "12.345.678", "skip": ["pupil"] }

→ 201 { "runId": "…", "participant": "P-3F9A2C1B" }

Autorización

Authorization: Bearer nsk_...

Los iframes no pueden setear headers. Usá api_key en la query; el servidor autoriza la sesión y redirige a /start sin la key.

Embed (iframe)

Dale al iframe permiso de cámara y sensores de movimiento:

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

Eventos en vivo (postMessage)

El iframe envía mensajes a su página padre: neuroscan:step-complete después de cada intento de un paso (un paso reintentado vuelve a llegar con un step.attempt mayor; vale el último), luego neuroscan:complete o neuroscan:error. Pedidos opcionales: neuroscan:display (brillo máximo/restaurar: el destello pupilar necesita una pantalla brillante) y neuroscan:keep-awake. Verificá siempre 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
  }
});

Payload del callback (v2)

Cuando termina el escaneo (o la persona lo cierra antes), NeuroScan hace POST al callback_url configurado en la API key. steps[] tiene un resultado por paso; metrics es la unión aplanada que se mantiene para integraciones anteriores.

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

Los admins pueden probar el escaneo completo sin session_id ni API key en /playground (requiere login).

/playground