# Kinometric API Reference — Mobile App

> **Base URL**: `https://kinometric.com/`
> **Protocol**: All endpoints accept **POST** with `Content-Type: application/json`
> **Auth**: Every request (except login and version check) requires `user_id` (int) + `encryption_key` (64-char hex string)
> **Practice Isolation**: Data is scoped to the user's active practice. Superadmins bypass all filters.

---

## Table of Contents

1. [Authentication](#1-authentication)
2. [Patient Management](#2-patient-management)
3. [Test Results](#3-test-results)
4. [Questionnaire](#4-questionnaire)
5. [CSV Upload](#5-csv-upload)
6. [AI / Analysis](#6-ai--analysis)
7. [Athena Integration (Mobile)](#7-athena-integration-mobile)
8. [Athena Integration (Dashboard Proxy)](#8-athena-integration-dashboard-proxy)
9. [User Management (Admin)](#9-user-management-admin)
10. [Email / PDF](#10-email--pdf)
11. [Version Check](#11-version-check)

---

## 1. Authentication

### POST `/back-auth.php` — Login

Authenticates a user and returns a new encryption key for subsequent requests. The encryption key is regenerated on every login.

**Required Parameters:**

| Parameter  | Type   | Description        |
|-----------|--------|--------------------|
| `username` | string | User's login name  |
| `password` | string | User's password    |

**Response (success):**
```json
{
  "success": true,
  "user_id": 3,
  "username": "efsi",
  "display_name": "Dr. Smith",
  "email": "user@example.com",
  "encryption_key": "a1b2c3d4...64_hex_chars",
  "practices": [
    {
      "practice_id": 1,
      "role": "admin",
      "is_default": true,
      "practice_name": "Main Practice",
      "has_athena": true,
      "department_id": 5,
      "department_name": "Physical Therapy",
      "athena_department_id": "150"
    }
  ],
  "active_practice_id": 1,
  "active_practice_name": "Main Practice"
}
```

**Response (error):**
```json
{"error": "Invalid username or password."}
```

**Notes:**
- The `encryption_key` is a 64-character hex string generated fresh each login.
- Store `user_id` and `encryption_key` for all subsequent API calls.
- The `practices` array contains all practices the user belongs to with their roles.
- `active_practice_id` is the default practice (first `is_default=true`, or first in list).

---

## 2. Patient Management

### POST `/back-get-patient-all.php` — Search Patients

Searches patients by name, date of birth, or external patient ID within the user's active practice.

**Required Parameters:**

| Parameter        | Type   | Description              |
|-----------------|--------|--------------------------|
| `user_id`        | int    | Authenticated user ID    |
| `encryption_key` | string | 64-char hex auth key     |

**Optional Parameters (at least one search field required):**

| Parameter        | Type   | Description                           |
|-----------------|--------|---------------------------------------|
| `first_name`     | string | Partial match (LIKE) on first name    |
| `last_name`      | string | Partial match (LIKE) on last name     |
| `date_of_birth`  | string | Exact match (YYYY-MM-DD)              |
| `realpatientid`  | string | Exact match on external patient ID    |
| `practice_id`    | int    | Override active practice (if allowed)  |

**Response (success):**
```json
{
  "success": true,
  "patients": [
    {
      "patient_id": 437,
      "first_name": "John",
      "last_name": "Doe",
      "date_of_birth": "1955-03-15",
      "realpatientid": "12345",
      "sex": "male",
      "is_sandbox": false
    }
  ]
}
```

**Notes:**
- Field names accept both underscore and dash formats (`encryption_key` or `encryption-key`).
- Search is case-insensitive and uses LIKE for name fields.

---

### POST `/back_addNewPatient.php` — Add New Patient

Creates a new patient record. Checks for duplicate `realPatientId`.

**Required Parameters:**

| Parameter        | Type   | Description                          |
|-----------------|--------|--------------------------------------|
| `user_id`        | int    | Authenticated user ID                |
| `encryption_key` | string | 64-char hex auth key                 |
| `first_name`     | string | Patient first name (min 2 chars)     |
| `last_name`      | string | Patient last name (min 2 chars)      |
| `dateOfBirth`    | string | Date of birth in `M/D/YYYY` format   |
| `realPatientId`  | string | External patient ID (min 2 chars)    |
| `sex`            | string | `male`, `female`, or `other`         |

**Response (success):**
```json
{
  "success": true,
  "duplicate": false,
  "message": "Patient added successfully.",
  "patients": [
    {
      "patient_id": 500,
      "first_name": "John",
      "last_name": "Doe",
      "date_of_birth": "1955-03-15",
      "realpatientid": "12345",
      "sex": "male"
    }
  ]
}
```

**Response (duplicate):**
```json
{
  "success": false,
  "duplicate": true,
  "message": "Duplicate realPatientId detected.",
  "patients": [
    {
      "patient_id": 123,
      "first_name": "John",
      "last_name": "Doe",
      "date_of_birth": "1955-03-15",
      "realpatientid": "12345",
      "sex": "male"
    }
  ]
}
```

**Important:** Note the camelCase field names (`dateOfBirth`, `realPatientId`) — this differs from other endpoints.

---

### POST `/test/back_patients.php` — Patient Management (Action-Based)

Full patient CRUD via action dispatch. More modern than the legacy endpoints above.

**Required Parameters:**

| Parameter        | Type   | Description              |
|-----------------|--------|--------------------------|
| `user_id`        | int    | Authenticated user ID    |
| `encryption_key` | string | 64-char hex auth key     |
| `action`         | string | One of: `list`, `add`, `update`, `delete` |

#### Action: `list`

| Parameter      | Type   | Default | Description                        |
|---------------|--------|---------|------------------------------------|
| `search`       | string | `""`    | Search first/last name or patient ID |
| `hide_sandbox` | bool   | false   | Exclude sandbox patients           |

**Response:**
```json
{
  "success": true,
  "patients": [
    {
      "patient_id": 437,
      "first_name": "John",
      "last_name": "Doe",
      "date_of_birth": "1955-03-15",
      "realpatientid": "12345",
      "sex": "male",
      "is_sandbox": false
    }
  ]
}
```

#### Action: `add`

| Parameter        | Type   | Required | Default     | Description               |
|-----------------|--------|----------|-------------|---------------------------|
| `first_name`     | string | yes      |             | Patient first name         |
| `last_name`      | string | yes      |             | Patient last name          |
| `date_of_birth`  | string | yes      |             | DOB in `YYYY-MM-DD`       |
| `realpatientid`  | string | no       | `"UNKNOWN"` | External patient ID        |
| `sex`            | string | no       | `"other"`   | `male`, `female`, `other`  |
| `is_sandbox`     | bool   | no       | false       | Mark as sandbox patient    |

**Response:**
```json
{
  "success": true,
  "patient_id": 500,
  "is_sandbox": false,
  "message": "Patient 'John Doe' created."
}
```

On duplicate `realpatientid`, returns:
```json
{
  "success": false,
  "error": "Patient ID '12345' already exists.",
  "existing_patient_id": 123
}
```

#### Action: `update`

| Parameter        | Type   | Required | Description                   |
|-----------------|--------|----------|-------------------------------|
| `patient_id`     | int    | yes      | Patient to update             |
| `first_name`     | string | no       | New first name                |
| `last_name`      | string | no       | New last name                 |
| `date_of_birth`  | string | no       | New DOB (`YYYY-MM-DD`)        |
| `realpatientid`  | string | no       | New external patient ID       |
| `sex`            | string | no       | `male`, `female`, `other`     |
| `is_sandbox`     | bool   | no       | Toggle sandbox status         |

#### Action: `delete`

| Parameter    | Type   | Required | Default | Description                          |
|-------------|--------|----------|---------|--------------------------------------|
| `patient_id` | int    | yes      |         | Patient to delete                    |
| `force`      | bool   | no       | false   | Required if patient has test results |

**Response:**
```json
{
  "success": true,
  "files_deleted": 4,
  "message": "Patient deleted. (12 test results also removed via cascade.) (4 CSV files removed.)"
}
```

---

## 3. Test Results

### POST `/back_test_results.php` — Save & Retrieve Test Results (Primary Endpoint)

The main endpoint for saving test results and retrieving historical data. One DB row per patient per day; multiple test types merge into the same row.

**Required Parameters:**

| Parameter        | Type   | Description                                       |
|-----------------|--------|---------------------------------------------------|
| `user_id`        | int    | Authenticated user ID                              |
| `encryption_key` | string | 64-char hex auth key                               |
| `patient_id`     | int    | Patient ID                                         |
| `test_type`      | string | `lbeo`, `rbeo`, `lbec`, `rbec` (or `lb-eo`, etc.) |

**Optional Parameters:**

| Parameter          | Type   | Default       | Description                                     |
|-------------------|--------|---------------|-------------------------------------------------|
| `test_results`     | object | null          | Score data to save (omit to retrieve only)       |
| `test_date`        | string | today         | Test date (`YYYY-MM-DD`)                         |
| `return_all_types` | bool   | false         | Return all 4 test types per row                  |
| `limit`            | int    | 2             | Number of historical tests (1-10)                |
| `date_from`        | string | null          | Start date filter (`YYYY-MM-DD`)                 |
| `date_to`          | string | null          | End date filter (`YYYY-MM-DD`)                   |
| `sort_order`       | string | `"desc"`      | `"asc"` or `"desc"`                             |
| `provider_name`    | string | auto-resolved | Provider attribution (falls back to display_name)|
| `provider_location`| string | auto-resolved | Location attribution (falls back to practice name)|

**`test_results` object structure (what the scoring engine produces):**
```json
{
  "movement_score": 8.5,
  "q_diff_range": 0.0023,
  "window_start": 49,
  "test_duration": 12.5,
  "sample_count": 1250,
  "avg_sample_rate": 100.0,
  "fatigue": "consistent",
  "max_stable_duration": 7.2,
  "scoring_mode": "qdiff",
  "duration_penalty": 0,
  "stability": {
    "percent_stable": 92.3,
    "continuous_duration": 7.2
  },
  "windows": {
    "3_second": { "q_diff_range": 0.0018, "start_sample": 49 },
    "5_second": { "q_diff_range": 0.0023, "start_sample": 49 },
    "7_second": { "q_diff_range": 0.0031, "start_sample": 49 }
  }
}
```

**Example Request — Save + Retrieve:**
```json
{
  "user_id": 3,
  "encryption_key": "a1b2c3...",
  "patient_id": 437,
  "test_type": "lbeo",
  "test_results": { "movement_score": 8.5, "..." : "..." },
  "return_all_types": true,
  "limit": 2
}
```

**Response:**
```json
{
  "success": true,
  "patient_id": 437,
  "test_type": "lbeo",
  "return_all_types": true,
  "limit": 2,
  "sort_order": "desc",
  "result_count": 2,
  "results": [
    {
      "test_id": 910,
      "test_date": "2026-02-23",
      "results_json": {
        "lbeo": { "movement_score": 8.5, "..." : "..." },
        "rbeo": { "movement_score": 7.2, "..." : "..." },
        "lbec": null,
        "rbec": null
      },
      "questions": { "id": 45, "hadfall": "no", "..." : "..." }
    },
    {
      "test_id": 905,
      "test_date": "2026-02-20",
      "results_json": { "..." : "..." }
    }
  ]
}
```

**DB Behavior:**
- Same patient + same date = UPDATE (merges test types into one row)
- Retesting same type same day = OVERWRITES that type in the row
- Different date = new row

---

### POST `/back_start_test_result.php` — Create Empty Test Row

Creates an empty test_results row for a patient/date. Used by the legacy flow to get a `test_id` before adding individual test types.

**Required Parameters:**

| Parameter        | Type   | Description                |
|-----------------|--------|----------------------------|
| `user_id`        | int    | Authenticated user ID      |
| `encryptionkey`  | string | 64-char hex auth key       |
| `patient_id`     | int    | Patient ID                 |

**Optional Parameters:**

| Parameter   | Type   | Default | Description             |
|------------|--------|---------|-------------------------|
| `test_date` | string | today   | Test date (`YYYY-MM-DD`) |

**Response (success):**
```json
{
  "success": true,
  "test_id": 912,
  "message": "Record successfully created."
}
```

**Response (duplicate):**
```json
{
  "success": false,
  "test_id": 910,
  "message": "Test already exists for this date."
}
```

**Note:** Uses `encryptionkey` (no underscore) — differs from most endpoints.

---

### POST `/backAddToTestResults.php` — Add Results to Existing Test (Legacy)

Updates an existing test row with results for a specific test type. Used in the legacy flow after `back_start_test_result.php`.

**Required Parameters:**

| Parameter          | Type   | Description                           |
|-------------------|--------|---------------------------------------|
| `user_id`          | int    | Authenticated user ID                 |
| `encryptionkey`    | string | 64-char hex auth key                  |
| `test_id`          | int    | Test row ID to update                 |
| `field`            | string | Test type: `lbeo`, `rbeo`, `lbec`, `rbec` |
| `json_data`        | object | Test results JSON                     |
| `new_notes`        | string | Notes for this test type              |
| `questions_id`     | int    | ID of linked questionnaire            |
| `providerName`     | string | Provider name                         |
| `providerLocation` | string | Provider location                     |

**Response:**
```json
{
  "success": true,
  "message": "Record updated successfully.",
  "test_id": 910
}
```

**Note:** Uses `encryptionkey` (no underscore). All parameters are required.

---

### POST `/backGetTestResults.php` — Get Last 5 Tests (Legacy)

Retrieves the last 5 test results for a patient with patient info, questions, and doctor details.

**Required Parameters:**

| Parameter        | Type   | Description           |
|-----------------|--------|-----------------------|
| `user_id`        | int    | Authenticated user ID |
| `encryptionkey`  | string | 64-char hex auth key  |
| `patient_id`     | int    | Patient ID            |

**Response:**
```json
{
  "success": true,
  "data": [
    {
      "test_id": 910,
      "test_date": "2026-02-23",
      "fall_risk": null,
      "bal_left_eo": null,
      "bal_right_eo": null,
      "questions": {
        "id": 45,
        "hadfall": "no",
        "numbness": "none",
        "dizziness": "none",
        "assistance": "none",
        "pain": 0,
        "exercise": "default",
        "lifestyle": 0,
        "bodypain": null
      },
      "patient": {
        "patient_id": 437,
        "first_name": "John",
        "last_name": "Doe",
        "realpatientid": "12345",
        "date_of_birth": "1955-03-15",
        "doctor": { "doctor_id": null, "doctor_name": null }
      }
    }
  ]
}
```

**Note:** This is a legacy endpoint. Prefer `back_test_results.php` with `return_all_types=true` and `limit=5`.

---

### POST `/backGetLastResults.php` — Get Last N Tests (Legacy)

Retrieves the last N test results with optional type filtering. Uses legacy individual-column format.

**Required Parameters:**

| Parameter        | Type   | Description           |
|-----------------|--------|-----------------------|
| `user_id`        | int    | Authenticated user ID |
| `encryptionkey`  | string | 64-char hex auth key  |
| `patient_id`     | int    | Patient ID            |

**Optional Parameters:**

| Parameter      | Type   | Default | Description                                       |
|---------------|--------|---------|---------------------------------------------------|
| `result_count` | int    | 2       | Number of results (1-5)                            |
| `test_type`    | string | `"all"` | `all`, `lb-eo`, `lb-ec`, `rb-eo`, `rb-ec`         |

**Response:**
```json
{
  "success": true,
  "patient_id": 437,
  "test_type": "all",
  "result_count": 2,
  "results": [
    {
      "test_id": 910,
      "test_date": "2026-02-23",
      "results_json": {
        "rbeo": { "..." : "..." },
        "lbeo": { "..." : "..." }
      }
    }
  ]
}
```

---

### POST `/backGetAllCurrentTestResults.php` — Get Latest Test (Legacy)

Returns the most recent test row for a patient with all 4 test type columns.

**Required Parameters:**

| Parameter        | Type   | Description                       |
|-----------------|--------|-----------------------------------|
| `userId`         | int    | Authenticated user ID (camelCase) |
| `encryptionKey`  | string | 64-char hex auth key (camelCase)  |
| `patientId`      | int    | Patient ID (camelCase)            |
| `fileName`       | string | CSV filename (required but unused for results) |

**Response:**
```json
{
  "success": true,
  "userID": 3,
  "encryptionKey": "a1b2c3...",
  "fileName": "437-lb-eo-23-2-2026.csv",
  "ibecResults": { "..." : "..." },
  "rbecResults": { "..." : "..." },
  "ibeoResults": { "..." : "..." },
  "rbeoResults": { "..." : "..." }
}
```

**Note:** Uses camelCase parameters. This is a legacy endpoint.

---

### POST `/back_get_last_test_id.php` — Get Most Recent Test ID

Returns the most recent test_id and test_date for a patient.

**Required Parameters:**

| Parameter        | Type   | Description           |
|-----------------|--------|-----------------------|
| `user_id`        | int    | Authenticated user ID |
| `encryption_key` | string | 64-char hex auth key  |
| `patient_id`     | int    | Patient ID            |

**Response:**
```json
{
  "success": true,
  "patient_id": 437,
  "test_id": 910,
  "test_date": "2026-02-23"
}
```

---

### POST `/backDeleteTestResult.php` — Delete a Test Result

Deletes a test result row by test_id.

**Required Parameters:**

| Parameter        | Type   | Description           |
|-----------------|--------|-----------------------|
| `user_id`        | int    | Authenticated user ID |
| `encryptionkey`  | string | 64-char hex auth key  |
| `test_id`        | int    | Test ID to delete     |

**Response:**
```json
{
  "success": true,
  "message": "Record with test_id 910 successfully deleted."
}
```

---

### POST `/test/back_get_test_details.php` — Full Test Details

Returns complete test data with patient info, AI analysis, questionnaire, and provider info.

**Required Parameters:**

| Parameter        | Type   | Description           |
|-----------------|--------|-----------------------|
| `user_id`        | int    | Authenticated user ID |
| `encryption_key` | string | 64-char hex auth key  |
| `patient_id`     | int    | Patient ID            |

**Optional Parameters:**

| Parameter | Type | Default         | Description                |
|----------|------|-----------------|----------------------------|
| `test_id` | int  | latest test     | Specific test to retrieve  |

**Response:**
```json
{
  "success": true,
  "patient": {
    "patient_id": 437,
    "first_name": "John",
    "last_name": "Doe",
    "date_of_birth": "1955-03-15",
    "realpatientid": "12345",
    "sex": "male"
  },
  "test": {
    "test_id": 910,
    "patient_id": 437,
    "test_date": "2026-02-23",
    "results_json": {
      "lbeo": { "movement_score": 8.5, "..." : "..." },
      "rbeo": { "movement_score": 7.2, "..." : "..." }
    },
    "ai_analysis": { "summary": "...", "icd10_codes": [], "interventions": [] },
    "ai_analysis_date": "2026-02-23 14:30:00",
    "ai_model": "claude-sonnet-4-20250514",
    "fall_risk": null,
    "providername": "Dr. Smith",
    "providerlocation": "Main Practice — PT",
    "questions_id": 45
  },
  "questions": {
    "id": 45,
    "patient_id": 437,
    "hadfall": "no",
    "numbness": "none",
    "dizziness": "none",
    "assistance": "none",
    "pain": 0,
    "exercise": "default",
    "lifestyle": 0,
    "bodypain": null,
    "created_at": "2026-02-23 14:25:00"
  }
}
```

---

### POST `/test/back_get_all_rankings.php` — All Patient Rankings

Returns one row per patient with their latest test scores. Used for rankings views and patient lists.

**Required Parameters:**

| Parameter        | Type   | Description           |
|-----------------|--------|-----------------------|
| `user_id`        | int    | Authenticated user ID |
| `encryption_key` | string | 64-char hex auth key  |

**Optional Parameters:**

| Parameter            | Type     | Description                                      |
|---------------------|----------|--------------------------------------------------|
| `date_from`          | string   | Filter tests from this date (`YYYY-MM-DD`)       |
| `date_to`            | string   | Filter tests to this date (`YYYY-MM-DD`)         |
| `min_score`          | float    | Minimum worst score                              |
| `max_score`          | float    | Maximum worst score                              |
| `risk_levels`        | string[] | Filter by risk: `["Critical","High","Moderate","Low"]` |
| `had_fall`           | bool     | Filter patients who reported falls               |
| `dizziness`          | bool     | Filter patients with dizziness                   |
| `numbness`           | bool     | Filter patients with numbness                    |
| `assistance`         | bool     | Filter patients needing assistance               |
| `balance_threshold`  | float    | Minimum left-right balance difference            |

**Response:**
```json
{
  "success": true,
  "count": 335,
  "rankings": [
    {
      "patient_id": 437,
      "test_id": 910,
      "test_date": "2026-02-23",
      "first_name": "John",
      "last_name": "Doe",
      "date_of_birth": "1955-03-15",
      "realpatientid": "12345",
      "sex": "male",
      "is_sandbox": false,
      "scores": {
        "lbeo": 8.5,
        "rbeo": 7.2,
        "lbec": null,
        "rbec": null
      },
      "best_score": 8.5,
      "worst_score": 7.2,
      "balance": 1.3,
      "percent_stable": 89.5,
      "max_duration": 6.8,
      "qdiff_3s": 0.0018,
      "qdiff_5s": 0.0023,
      "qdiff_7s": 0.0031,
      "risk_level": "Moderate",
      "has_ai": true,
      "questionnaire": {
        "hadfall": "no",
        "dizziness": "none",
        "numbness": "none",
        "assistance": "none",
        "pain": 0,
        "lifestyle": 0
      }
    }
  ]
}
```

---

## 4. Questionnaire

### POST `/backInsertQuestion.php` — Save Questionnaire

Saves pre-test questionnaire answers. Automatically links to today's test_results row if one exists.

**Required Parameters:**

| Parameter        | Type   | Description           |
|-----------------|--------|-----------------------|
| `user_id`        | int    | Authenticated user ID |
| `encryptionkey`  | string | 64-char hex auth key  |

**Optional Parameters:**

| Parameter    | Type   | Default     | Description                               |
|-------------|--------|-------------|-------------------------------------------|
| `patient_id` | int    | null        | Patient ID (needed for auto-link)          |
| `hadfall`    | string | `"default"` | Had a fall? (`"yes"`, `"no"`, `"default"`) |
| `numbness`   | string | `"none"`    | Experiences numbness?                      |
| `dizziness`  | string | `"none"`    | Experiences dizziness? (also accepts `dizzyness`) |
| `assistance` | string | `"none"`    | Needs walking assistance?                  |
| `pain`       | float  | 0.0         | Pain level (0-10)                          |
| `exercise`   | string | `"default"` | Exercise frequency                         |
| `lifestyle`  | float  | 0.0         | Lifestyle impact score                     |
| `bodypain`   | string | null        | Body pain location/description             |

**Response:**
```json
{
  "success": true,
  "question_id": 46,
  "message": "Question created successfully.",
  "patient_id": 437,
  "linked_test_id": 910
}
```

**Notes:**
- Uses `encryptionkey` (no underscore).
- If a test_results row exists for this patient today, the questionnaire is automatically linked via `questions_id`.
- The `linked_test_id` field is only present if auto-linking succeeded.

---

### POST `/backGetQuestions.php` — Get Current & Previous Questionnaire

Retrieves the two most recent questionnaires for a patient (for showing changes).

**Required Parameters:**

| Parameter        | Type   | Description                       |
|-----------------|--------|-----------------------------------|
| `userId`         | int    | Authenticated user ID (camelCase) |
| `encryptionKey`  | string | 64-char hex auth key (camelCase)  |
| `patientId`      | int    | Patient ID (camelCase)            |
| `fileName`       | string | CSV filename (required)           |

**Response:**
```json
{
  "success": true,
  "userID": 3,
  "encryptionKey": "a1b2c3...",
  "fileName": "437-lb-eo-23-2-2026.csv",
  "currentQuestions": {
    "id": 46,
    "patient_id": 437,
    "hadfall": "no",
    "numbness": "none",
    "dizziness": "none",
    "assistance": "none",
    "pain": 0,
    "exercise": "daily",
    "lifestyle": 2,
    "bodypain": null
  },
  "previousQuestions": {
    "id": 40,
    "patient_id": 437,
    "hadfall": "yes",
    "..." : "..."
  }
}
```

**Note:** Uses camelCase parameter names. The `fileName` parameter is required but only used for context.

---

## 5. CSV Upload

### POST `/back_upload_csv.php` — Upload CSV Sensor Data

Uploads a base64-encoded CSV file from the mobile device. Files are saved to the date-organized data directory and copied to the allfiles directory for scoring.

**Required Parameters:**

| Parameter        | Type   | Description                                |
|-----------------|--------|--------------------------------------------|
| `user_id`        | int    | Authenticated user ID                      |
| `encryption_key` | string | 64-char hex auth key                       |
| `filename`       | string | CSV filename (e.g., `437-lb-eo-23-2-2026.csv`) |
| `content`        | string | Base64-encoded CSV content                 |

**Optional Parameters:**

| Parameter    | Type   | Description                          |
|-------------|--------|--------------------------------------|
| `patient_id` | int    | Patient ID (for access check)        |
| `test_type`  | string | Test type (`lb-eo`, `rb-eo`, etc.)   |
| `notes`      | string | Questionnaire answers as CSV string  |

**Response (success):**
```json
{
  "success": true,
  "path": "2026/February/23/437-lb-eo-23-2-2026.csv",
  "bytes": 125430,
  "patient_id": 437,
  "test_type": "lb-eo"
}
```

**Notes:**
- Filename must contain only alphanumeric, dash, underscore, and dot characters.
- `.csv` extension is added automatically if missing.
- Max file size: 10MB.
- The `notes` parameter is prepended as header rows in the saved CSV file (questionnaire answers for the scoring engine).
- Also accepts `encryptionkey` (no underscore) as an alternative to `encryption_key`.
- Files are saved to `/home/kinometric/data/YYYY/Month/DD/` and copied to `/home/kinometric/learn/allfiles/`.

**CSV Filename Format:** `{patient_id}-{test_type}-{day}-{month}-{year}.csv`
- Example: `437-lb-eo-23-2-2026.csv` (patient 437, left balance eyes open, Feb 23, 2026)

---

## 6. AI / Analysis

### POST `/back_analyze_patient_ai.php` — AI-Powered Analysis (Claude)

Generates a comprehensive clinical analysis using Claude AI. Results are cached in the database to avoid duplicate API costs (~$0.015 per analysis).

**Required Parameters:**

| Parameter        | Type   | Description           |
|-----------------|--------|-----------------------|
| `user_id`        | int    | Authenticated user ID |
| `encryption_key` | string | 64-char hex auth key  |
| `test_id`        | int    | Test ID to analyze    |

**Optional Parameters:**

| Parameter          | Type | Default | Description                      |
|-------------------|------|---------|----------------------------------|
| `force_reanalysis` | bool | false   | Bypass cache and regenerate      |

**Response:**
```json
{
  "success": true,
  "test_id": 910,
  "patient_name": "John Doe",
  "age": 71,
  "test_date": "2026-02-23",
  "results_json": { "lbeo": { "..." : "..." }, "rbeo": { "..." : "..." } },
  "metrics": {
    "overall_score": 7.2,
    "left_leg": { "score": 8.5, "test_duration": 12.5, "..." : "..." },
    "right_leg": { "score": 7.2, "test_duration": 11.8, "..." : "..." },
    "has_asymmetry": false,
    "asymmetry_ratio": 1.2
  },
  "analysis": "Patient shows moderate balance impairment...",
  "icd10_codes": ["R26.81", "R29.6"],
  "interventions": ["Balance training 3x/week", "Gait assessment"],
  "risk_level": "Moderate",
  "trend": "Stable",
  "analysis_type": "ai_powered",
  "ai_model": "claude-sonnet-4-20250514",
  "from_cache": true,
  "ai_analysis_date": "2026-02-23 14:30:00",
  "ai_analysis_json": "{\"summary\":\"...\",\"risk_level\":\"Moderate\",\"trend\":\"Stable\",\"icd10_codes\":[...],\"interventions\":[...]}"
}
```

**Notes:**
- The `ai_analysis_json` field is a pre-serialized JSON string convenient for the Flutter PDF generator.
- `from_cache` indicates whether the result came from the database cache or was freshly generated.
- Risk levels: `Low` (>8), `Moderate` (6-8), `High` (4-6), `CRITICAL` (<4).

---

### POST `/back_py-analysis.php` — Rule-Based Analysis (No AI Cost)

Identical interface to `back_analyze_patient_ai.php` but uses a deterministic Python rule engine instead of Claude AI. Free, instant, consistent results.

**Parameters:** Same as `back_analyze_patient_ai.php`.

**Response:** Same structure. Key differences:
- `analysis_type`: `"py_analysis"` (instead of `"ai_powered"`)
- `ai_model`: `"py-analysis"`
- No API cost

---

### POST `/back_analyze_patient.php` — Automated Analysis (Legacy)

Older analysis endpoint that runs a Python script for rule-based analysis. Does not cache results.

**Required Parameters:**

| Parameter        | Type   | Description           |
|-----------------|--------|-----------------------|
| `user_id`        | int    | Authenticated user ID |
| `encryption_key` | string | 64-char hex auth key  |
| `test_id`        | int    | Test ID to analyze    |

**Response:**
```json
{
  "success": true,
  "test_id": 910,
  "patient_name": "John Doe",
  "age": 71,
  "test_date": "2026-02-23",
  "metrics": { "..." : "..." },
  "analysis": "Analysis text...",
  "analysis_type": "automated"
}
```

---

### POST `/back_validate_test.php` — Validate Test Data Quality

Quick sanity check on test data quality. Validates sample counts, durations, sample rates, and compares to previous test.

**Required Parameters:**

| Parameter        | Type   | Description           |
|-----------------|--------|-----------------------|
| `user_id`        | int    | Authenticated user ID |
| `encryption_key` | string | 64-char hex auth key  |
| `test_id`        | int    | Test ID to validate   |

**Response:**
```json
{
  "success": true,
  "test_id": 910,
  "patient_id": 437,
  "patient_name": "John Doe",
  "test_date": "2026-02-23",
  "validation": {
    "status": "pass",
    "message": "Test data quality is good",
    "issues": [],
    "legs_tested": ["Left Eyes Open", "Right Eyes Open"],
    "legs_tested_count": 2,
    "data_quality": [
      {
        "leg": "Left Eyes Open",
        "status": "pass",
        "sample_count": 1250,
        "test_duration": 12.5,
        "sample_rate": 100.0,
        "issues": []
      }
    ]
  },
  "comparison": {
    "available": true,
    "previous_test_id": 905,
    "previous_test_date": "2026-02-20",
    "summary": "Stable - similar to previous test",
    "changes": [
      {
        "leg": "Left Eyes Open",
        "previous_score": 8.2,
        "current_score": 8.5,
        "change": 0.3,
        "change_percent": 3.7,
        "status": "stable"
      }
    ]
  }
}
```

**Validation thresholds:**
- Minimum samples: 250 (error if below)
- Optimal samples: 500 (warning if below)
- Minimum duration: 3.0 seconds (warning if below)
- Minimum sample rate: 90 Hz (warning if below)

---

### POST `/backAnalyzeResults.php` — Analyze CSV File (Legacy)

Runs the legacy Python `findStable.py` analysis on a CSV file. Locates the file by name in the data directories.

**Required Parameters:**

| Parameter        | Type   | Description                       |
|-----------------|--------|-----------------------------------|
| `user_id`        | int    | Authenticated user ID             |
| `encryptionkey`  | string | 64-char hex auth key              |
| `fileName`       | string | CSV filename to analyze           |

**Response:** Returns the raw JSON output from the Python analysis script.

---

## 7. Athena Integration (Mobile)

### POST `/back_athena.php` — Mobile Athena Gateway

Provides athenaOne API access for the Flutter mobile app. Action-dispatch endpoint. Requires the active practice to have athena configuration in the database.

**Required Parameters (all actions):**

| Parameter        | Type   | Description           |
|-----------------|--------|-----------------------|
| `user_id`        | int    | Authenticated user ID |
| `encryption_key` | string | 64-char hex auth key  |
| `action`         | string | See actions below     |

#### Action: `search_patient`

Search athenaOne patients by name.

| Parameter       | Type   | Required | Description                    |
|----------------|--------|----------|--------------------------------|
| `last_name`     | string | yes      | Patient last name              |
| `first_name`    | string | no       | Patient first name             |
| `dob`           | string | no       | Date of birth                  |
| `department_id` | int    | no       | Department (default from config) |

**Response:**
```json
{
  "success": true,
  "patients": [ { "patientid": "7681", "firstname": "John", "lastname": "Doe", "..." : "..." } ],
  "total": 3
}
```

#### Action: `get_patient`

Get full demographics for a single athena patient.

| Parameter            | Type | Required | Description           |
|---------------------|------|----------|-----------------------|
| `athena_patient_id`  | int  | yes      | Athena patient ID     |

**Response:**
```json
{
  "success": true,
  "patient": {
    "patientid": "7681",
    "firstname": "John",
    "lastname": "Doe",
    "dob": "03/15/1955",
    "sex": "M",
    "departmentid": "1",
    "..." : "..."
  }
}
```

#### Action: `get_departments`

List all departments for the practice.

**Response:**
```json
{
  "success": true,
  "departments": [ { "departmentid": "1", "name": "Main Office", "..." : "..." } ],
  "default_department_id": "1"
}
```

---

### POST `/back_athena_send.php` — Upload PDF to Athena

Uploads a PDF as a clinical document to a patient's chart in athenaOne.

**Required Parameters:**

| Parameter            | Type   | Description                    |
|---------------------|--------|--------------------------------|
| `user_id`            | int    | Authenticated user ID          |
| `encryption_key`     | string | 64-char hex auth key           |
| `athena_patient_id`  | string | Athena patient ID              |
| `patient`            | string | Patient display name           |
| `pdf`                | string | Base64-encoded PDF (or data URI) |

**Optional Parameters:**

| Parameter       | Type   | Default        | Description                    |
|----------------|--------|----------------|--------------------------------|
| `department_id` | string | config default | Athena department ID           |

**Response:**
```json
{
  "success": true,
  "clinicaldocumentid": 212361,
  "message": "Document uploaded to athenaOne successfully.",
  "athena_patient_id": "7681",
  "department_id": "1"
}
```

---

## 8. Athena Integration (Dashboard Proxy)

### POST `/test/back_athena_proxy.php` — Full Athena Proxy

Comprehensive athenaOne proxy used by the web dashboard. Supports many more actions than the mobile gateway.

**Required Parameters (all actions):**

| Parameter        | Type   | Description           |
|-----------------|--------|-----------------------|
| `user_id`        | int    | Authenticated user ID |
| `encryption_key` | string | 64-char hex auth key  |
| `action`         | string | See actions below     |

**Available Actions:**

| Action                   | Description                                      |
|-------------------------|--------------------------------------------------|
| `ping`                   | Test athena connection, returns token info         |
| `search_patient`         | Look up patient in Kino DB + athena by ID          |
| `get_departments`        | List all departments                               |
| `get_documents`          | Get clinical documents for a patient               |
| `download_document`      | Download a specific document as base64 PDF         |
| `upload_test`            | Upload PDF to patient's chart                      |
| `get_chart_data`         | Get clinical chart data (problems, meds, allergies, vitals, etc.) |
| `get_lab_results`        | Get lab results (list or drill into analyte)        |
| `list_patients`          | Browse athena patients with pagination              |
| `search_athena_patients` | Search athena patients by name                      |
| `get_athena_patient`     | Get full demographics for one patient               |
| `get_practice_info`      | Get practice info from athena                       |
| `get_log`                | Read upload log entries                              |
| `get_audit_log`          | Read HIPAA access audit log                          |
| `log_incident`           | Record a security incident                           |
| `get_incidents`          | Read incident log entries                            |

#### Key Actions for Mobile:

**`search_patient`** — Look up by Kino + athena:
```json
{ "action": "search_patient", "athena_patient_id": 7681, "..." : "..." }
```
Returns `{ "kinometric": {...}, "athena": {...} }`.

**`upload_test`** — Upload PDF to athena chart:
```json
{
  "action": "upload_test",
  "athena_patient_id": 7681,
  "patient": "John Doe",
  "pdf": "JVBERi0xLjQK...",
  "department_id": 1
}
```

**`get_chart_data`** — Generic chart data with whitelisted resources:
```json
{ "action": "get_chart_data", "athena_patient_id": 7681, "resource": "problems" }
```
Allowed resources: `encounters`, `procedures`, `problems` (all others are rejected).

---

## 9. User Management (Admin)

### POST `/test/back_users.php` — User Management

Admin-only endpoint. Requires the authenticated user to have `is_admin=true`.

**Required Parameters:**

| Parameter        | Type   | Description           |
|-----------------|--------|-----------------------|
| `user_id`        | int    | Authenticated user ID |
| `encryption_key` | string | 64-char hex auth key  |
| `action`         | string | See actions below     |

#### Action: `list`

Returns all users with their practice memberships.

**Response:**
```json
{
  "success": true,
  "users": [
    {
      "user_id": 3,
      "username": "efsi",
      "display_name": "Dr. Smith",
      "email": "user@example.com",
      "is_admin": true,
      "practices": [
        {
          "practice_id": 1,
          "practice_name": "Main Practice",
          "role": "admin",
          "is_default": true,
          "department_id": 5,
          "department_name": "Physical Therapy"
        }
      ]
    }
  ]
}
```

#### Action: `list_practices`

Returns all active practices with their departments.

**Response:**
```json
{
  "success": true,
  "practices": [
    {
      "id": 1,
      "name": "Main Practice",
      "has_athena": true,
      "departments": [
        { "id": 5, "name": "Physical Therapy", "athena_department_id": "150" }
      ]
    }
  ]
}
```

#### Action: `add`

| Parameter      | Type     | Required | Description                              |
|---------------|----------|----------|------------------------------------------|
| `username`     | string   | yes      | Login username                           |
| `password`     | string   | yes      | Must meet HIPAA policy (8+ chars, upper/lower/number) |
| `email`        | string   | no       | User email                               |
| `display_name` | string   | no       | Display name (defaults to username)       |
| `is_admin`     | bool     | no       | Admin flag                               |
| `practices`    | array    | no       | Practice memberships (defaults to practice 1) |

`practices` array format:
```json
[
  {
    "practice_id": 1,
    "role": "clinician",
    "is_default": true,
    "department_id": 5
  }
]
```

#### Action: `update`

| Parameter        | Type   | Required | Description               |
|-----------------|--------|----------|---------------------------|
| `target_user_id` | int    | yes      | User to update            |
| `username`       | string | no       | New username              |
| `email`          | string | no       | New email                 |
| `display_name`   | string | no       | New display name          |
| `is_admin`       | bool   | no       | Admin flag                |
| `practices`      | array  | no       | Replace practice memberships |

#### Action: `reset_password`

| Parameter        | Type   | Required | Description                                  |
|-----------------|--------|----------|----------------------------------------------|
| `target_user_id` | int    | yes      | User whose password to reset                 |
| `password`       | string | yes      | New password (must meet HIPAA policy)         |

#### Action: `delete`

| Parameter        | Type | Required | Description      |
|-----------------|------|----------|------------------|
| `target_user_id` | int  | yes      | User to delete   |

Cannot delete your own account.

---

## 10. Email / PDF

### POST `/back_mail_pdf.php` — Email PDF Results

Sends a balance test PDF report as an email attachment via Microsoft Graph API. The PDF is encrypted with a password before sending.

**Required Parameters:**

| Parameter        | Type   | Description                       |
|-----------------|--------|-----------------------------------|
| `user_id`        | int    | Authenticated user ID             |
| `encryption_key` | string | 64-char hex auth key              |
| `emailAddress`   | string | Recipient email address           |
| `patient`        | string | Patient display name              |
| `pdf`            | string | Base64-encoded PDF (or data URI)  |

**Response:**
```json
{
  "success": true,
  "message": "Email sent successfully (Graph).",
  "recipient": "patient@example.com",
  "subject": "Kinometric Balance Test Results - 02/23/2026"
}
```

**Notes:**
- The PDF is encrypted with AES-256 before sending (password: `kino1$`).
- Uses Microsoft Graph API with client credentials (server-side config).
- Also accepts `encryptionkey` (no underscore).

---

## 11. Version Check

### GET/POST `/back_check_version.php` — App Version Check

Checks if the mobile app needs an update. No database dependency. Can be called via GET or POST.

**Parameters:**

| Parameter         | Type   | Required | Description               |
|------------------|--------|----------|---------------------------|
| `current_version` | string | yes      | App's current version (e.g., `"1.0.1"`) |

**GET example:** `?current_version=1.0.1`

**POST example:**
```json
{ "current_version": "1.0.1" }
```

**Response:**
```json
{
  "update_required": false,
  "update_available": true,
  "latest_version": "1.0.2",
  "min_required_version": "1.0.0",
  "current_version": "1.0.1",
  "apk_url": "https://kinometric.com/app.apk",
  "apk": "https://kinometric.com/app.apk",
  "release_notes": "Bug fixes and performance improvements"
}
```

**Notes:**
- No authentication required.
- `update_required` = true means the app MUST update (breaking API changes).
- `update_available` = true means a newer version exists (optional).
- `apk` is a duplicate of `apk_url` for FlutterFlow compatibility.
- Version config is read from `version_config.json` on the server.

---

## Auth Key Naming Conventions

Different endpoints use different parameter names for the encryption key. Here's the mapping:

| Endpoint Group                          | Key Parameter Name  |
|----------------------------------------|---------------------|
| `back-auth.php` (login response)        | `encryption_key`    |
| `back_test_results.php`                 | `encryption_key`    |
| `back_upload_csv.php`                   | `encryption_key` or `encryptionkey` |
| `back_addNewPatient.php`                | `encryption_key`    |
| `back-get-patient-all.php`              | `encryption_key`    |
| `back_get_last_test_id.php`             | `encryption_key`    |
| `back_analyze_patient_ai.php`           | `encryption_key`    |
| `back_validate_test.php`                | `encryption_key`    |
| `back_py-analysis.php`                  | `encryption_key`    |
| `back_mail_pdf.php`                     | `encryption_key` or `encryptionkey` |
| `back_athena.php`                       | `encryption_key`    |
| `back_athena_send.php`                  | `encryption_key` or `encryptionkey` |
| `test/back_patients.php`                | `encryption_key`    |
| `test/back_users.php`                   | `encryption_key`    |
| `test/back_athena_proxy.php`            | `encryption_key`    |
| `test/back_get_test_details.php`        | `encryption_key`    |
| `test/back_get_all_rankings.php`        | `encryption_key`    |
| `back_start_test_result.php`            | `encryptionkey`     |
| `backAddToTestResults.php`              | `encryptionkey`     |
| `backGetTestResults.php`                | `encryptionkey`     |
| `backGetLastResults.php`                | `encryptionkey`     |
| `backDeleteTestResult.php`              | `encryptionkey`     |
| `backInsertQuestion.php`                | `encryptionkey`     |
| `backGetQuestions.php`                   | `encryptionKey` (camelCase) |
| `backGetAllCurrentTestResults.php`       | `encryptionKey` (camelCase) |
| `backAnalyzeResults.php`                | `encryptionkey`     |

**Recommendation for mobile app:** Use `encryption_key` (with underscore) as the standard. The newer endpoints all use this format. For legacy endpoints that require `encryptionkey`, send it as `encryptionkey` (no underscore).

---

## Practice Isolation

All endpoints that access patient or test data apply practice-based isolation:

1. **Active practice resolution** (in order):
   - Explicit `practice_id` or `active_practice_id` in the request
   - Session (web only)
   - User's default practice (`is_default=true` in `user_practices`)
   - First practice in user's membership list

2. **Superadmin bypass**: Users with `is_admin=true` can see all data across all practices.

3. **To switch practices**, pass `practice_id` in any request. The user must be a member of that practice (or be a superadmin).

---

## Test Types

| Code    | CSV Filename | Description             |
|---------|-------------|-------------------------|
| `lbeo`  | `lb-eo`     | Left Balance, Eyes Open  |
| `rbeo`  | `rb-eo`     | Right Balance, Eyes Open |
| `lbec`  | `lb-ec`     | Left Balance, Eyes Closed|
| `rbec`  | `rb-ec`     | Right Balance, Eyes Closed|

`back_test_results.php` accepts both formats (with or without hyphen) and normalizes to the 4-letter format.

---

## Score & Risk Reference

| Score Range | Color  | Risk Level |
|------------|--------|------------|
| >= 8       | Green  | Low        |
| 6 - 7.99  | Green  | Moderate   |
| 4 - 5.99  | Orange | High       |
| < 4        | Red    | Critical   |

Score range is 0-10. Score = Raw (no rescale in production).

---

## New Patient Intake — Two-Tab Design

The "Add Patient" screen has two tabs. The app remembers which tab the user last selected permanently via `SharedPreferences` (`new_patient_tab` key, values `"manual"` or `"athena"`). On screen load, restore the last-used tab. Save on every tab switch.

### Tab 1: "Manual Entry"

For standalone patients (no athenaOne link) or practices without EMR integration.

**Required fields:** first name, last name, DOB (date picker → `YYYY-MM-DD`), sex (male/female/other dropdown)

**Optional fields:** address, phone, email

**On submit:**
```
POST /test/back_patients.php
{
  "user_id": <stored>,
  "encryption_key": <stored>,
  "action": "add",
  "first_name": "John",
  "last_name": "Doe",
  "date_of_birth": "1955-03-15",
  "sex": "male"
}
```

**Success response:** `{ "success": true, "patient_id": 500 }` → navigate to test screen with this patient.

**Duplicate response:** `{ "success": false, "existing_patient_id": 123, "error": "Patient ID '12345' already exists." }` → show dialog: "This patient already exists. Use existing patient?" → if yes, navigate with `existing_patient_id`.

### Tab 2: "Import from athenaOne"

For patients in the athenaOne EMR. Primary path is lookup by athena patient ID. Name search is a fallback.

**Prerequisites:**
- Practice must have athena configured (`has_athena: true` in login response practices array).
- If `has_athena` is false for all user practices, **hide this tab entirely**.

**Primary Path: Athena Patient ID Lookup** (single input field at top, prominent)
- Text field: "athenaOne Patient ID"
- This is the recommended flow. **No department ID is needed.** One API call, one audit log entry.
- On submit:
```
POST /back_athena.php
{
  "user_id": <stored>,
  "encryption_key": <stored>,
  "action": "get_patient",
  "athena_patient_id": 7681
}
```
- Success → show confirmation card with demographics
- The response includes `primarydepartmentid` — **store this** for the import (see below)

**Fallback Path: Name + DOB Search** (below the ID field, less prominent)

When the clinician doesn't have the athena ID, search by name. This is a two-step fallback:

**Step 1: Search Kino DB first** (free, no athena API call, no department needed):
```
POST /back-get-patient-all.php
{
  "user_id": <stored>,
  "encryption_key": <stored>,
  "first_name": "John",
  "last_name": "Doe"
}
```
If found → patient was previously imported, no need to re-import. Use the existing patient.

**Step 2: If not in Kino, search athena** (requires department):
```
POST /back_athena.php
{
  "user_id": <stored>,
  "encryption_key": <stored>,
  "action": "search_patient",
  "last_name": "Doe",
  "first_name": "John",
  "department_id": <from SharedPreferences>
}
```
- Returns list of matches. **DOB filtering is client-side** — athena search API does not support DOB as a filter parameter. Filter results where `dob` matches the entered DOB (athena returns DOB as `MM/DD/YYYY`).
- Show results in a selectable list (name, DOB, athena ID). User taps to select.
- Selected patient → call `get_patient` for full demographics → show confirmation card.

**IMPORTANT: Why department matters for name search only.**
athena's `/patients` search endpoint requires a `departmentid` and scopes results to patients registered at that office. Different offices return completely different patient lists with zero overlap. The ID lookup (`get_patient`) does NOT require a department — it returns the patient regardless of office. This is why ID lookup is the primary path.

**Confirmation Card** (shown after ID lookup or name search):
- Display: full name, DOB, sex, athena patient ID, office/department
- Button: "Import Patient"
- On import — **include `athena_department_id` from the patient's record:**
```
POST /test/back_patients.php
{
  "user_id": <stored>,
  "encryption_key": <stored>,
  "action": "add",
  "first_name": <from athena>,
  "last_name": <from athena>,
  "date_of_birth": "1955-03-15",        ← convert from athena "03/15/1955"
  "sex": "male",                         ← map: M→male, F→female, else→other
  "realpatientid": "7681",               ← athena patientid
  "athena_department_id": "1",           ← from athena primarydepartmentid
  "is_sandbox": false                    ← true only for sandbox testing
}
```
- The server resolves `athena_department_id` to our internal `department_id` and stores both on the patient record. This stored department is used for all future athena operations (PDF upload, chart data) so the app never needs to ask for it again.
- **Duplicate handling:** Same as Tab 1 — if `existing_patient_id` returned, offer to use the existing patient.
- Success → navigate to test screen with the new `patient_id`.

**DOB Format Conversion:**
- Athena returns: `MM/DD/YYYY` (e.g., `"03/15/1955"`)
- Kino DB expects: `YYYY-MM-DD` (e.g., `"1955-03-15"`)
- Convert before POSTing to `back_patients.php`

**Sex Mapping:**
- Athena returns: `"M"`, `"F"`, or other values
- Kino DB expects: `"male"`, `"female"`, `"other"`
- Map: `M` → `male`, `F` → `female`, anything else → `other`

---

## Department — When It Matters and When It Doesn't

Departments in athena = physical office locations. A practice has multiple offices. Patients are registered to a specific office. Clinicians rotate between offices.

**When department is NOT needed:**
- `get_patient` by athena patient ID — works without department, returns full demographics
- Searching the Kino database — practice isolation handles this, no athena call
- Most of the mobile app flow — the patient's department is stored on import

**When department IS needed:**
- `search_patient` by name — athena requires a `departmentid` and scopes results to that office only. Different offices return completely different patient lists with zero overlap.
- `upload_test` (PDF to athena) — requires `departmentid`. Use the one stored on the patient record.
- `get_chart_data`, `get_documents` — require `departmentid`. Use stored value.

**How the app gets the department:**
1. **On patient import:** The `get_patient` response includes `primarydepartmentid`. Send this as `athena_department_id` when creating the patient. The server stores it.
2. **For existing imported patients:** The `department_id` is already on the patient record in the Kino DB. The server uses it for athena operations.
3. **For name search fallback only:** The clinician's "current office" is needed. Store in `SharedPreferences` key `kino_athena_dept`.

**Setting the clinician's office (for name search fallback only):**

This is only needed if the user tries the name search fallback. It is NOT needed for the primary ID lookup flow.

**Storage:** `SharedPreferences` key `kino_athena_dept` (athena department ID as a string)

**When to prompt:**
- Only when the user tries a name search and no department is stored
- Or from Settings → "Change Office"
- Never on the primary ID lookup path

**How to populate the picker:**
```
POST /back_athena.php
{
  "user_id": <stored>,
  "encryption_key": <stored>,
  "action": "get_departments"
}
```
Returns: `{ "success": true, "departments": [{"departmentid": "1", "name": "Main Office"}, ...] }`

Show as a simple list. User taps one → store `departmentid` in SharedPreferences → done.

**Alternative:** The login response includes `athena_department_id` per practice in the `practices` array. If set, use it as the default without prompting.

---

## Returning Patient Lookup

Before running a test on an existing patient, search the Kino database:

```
POST /back-get-patient-all.php
{
  "user_id": <stored>,
  "encryption_key": <stored>,
  "last_name": "Doe"
}
```

Returns patients within the user's active practice. Select one → proceed to questionnaire and test.

---

## Recommended Mobile App Flow

1. **Login**: `POST /back-auth.php` → store `user_id`, `encryption_key`, `practices`, `display_name`
2. **Find or create patient** (one of these paths):
   - **Returning patient**: `POST /back-get-patient-all.php` with `last_name` → select from list
   - **New patient (manual)**: `POST /test/back_patients.php` action:add
   - **New patient (athena import — primary)**: `POST /back_athena.php` action:get_patient with `athena_patient_id` → confirm → `POST /test/back_patients.php` action:add with `realpatientid` + `athena_department_id`
   - **New patient (athena import — fallback)**: Search Kino DB first → if not found, `POST /back_athena.php` action:search_patient with `last_name` + `department_id` → select → confirm → import
3. **Save questionnaire**: `POST /backInsertQuestion.php`
4. **Collect test data** (device sensors → CSV)
5. **Score locally** (BalanceScorer.dart or BalanceScorer.java)
6. **Save results**: `POST /back_test_results.php` with `test_results` object
7. **Upload CSV**: `POST /back_upload_csv.php` with base64 content
8. **Validate**: `POST /back_validate_test.php` with `test_id`
9. **Get AI analysis**: `POST /back_analyze_patient_ai.php` with `test_id`
10. **Email PDF**: `POST /back_mail_pdf.php` with base64 PDF
11. **Upload to athena** (optional): `POST /back_athena_send.php` — uses `department_id` stored on the patient record
