# Rune Labs Data API

The Rune Labs data API allows patient datasets to be uploaded to the Rune platform from a mobile device, or any other environment. Data can be uploaded directly and securely via HTTPS from anywhere, given proper authentication. It also allows read access to patient upload statistics.

Each endpoint is documented in detail, below.

# Overview

## Key Concepts

### Patient

The person about whom data was measured. This may include a [StrivePD](https://www.strive.group/) user, a person with a neural implant, etc. Each patient has a unique identifier ("patient ID"). Patient IDs are generated by Rune and cannot be changed.

### Device

The sensor serving as the data source (e.g. a DBS implant, iOS application, wearable device, etc.). A patient may have multiple devices: each one has a unique identifier ("device ID"). Device IDs are generated by Rune and cannot be changed. They do not correspond to any identifier or information from the device itself.

### Client

Refers to the patient-facing app itself. A client accesses data programmatically, on behalf of a patient.

A client is associated with exactly one patient. Patients may have multiple clients, depending on how many apps are sending or accessing data. Patients and clients are resources in the Rune platform with unique identifiers, and they are used to control access to patient data.

### Dataset

A dataset is a collection of files containing data recorded by a patient's device, over some period of time.

Each dataset has a **schema** that broadly identifies its contents. The `schema_id` serves as a typing system for datasets. Supported schema IDs include:

- `medtronic-percept-1`
- `medtronic-rcs-1`
- `strivestudy-applewatch-1`
- `strivestudy-applewatch-md-1`
- `strivestudy-healthkit-1`
- `strivestudy-healthkit-2`
- `strivestudy-spiraltest-1`

Schemas specify the **number of files** that each dataset of a particular type contains, as well as the semantics of each of those files (i.e. what the file is expected to contain). This is used to parse and ingest data after upload.

For example, the `strivestudy-applewatch-md-1` dataset schema is used for data collected by the StrivePD app, from the Apple Watch Movement Disorder Kit. Per the schema definition, 3 files must be uploaded for each dataset:

- `tremor` \- Tremor data
- `dyskinetic` \- Dyskinesia data
- `metadata` \- Assorted metadata

## Authentication

Authentication is required for all API requests. There are several authentication methods:

### Client Keys

Client keys provide read _and_ write access to **one patient**.

To use a client key pair for authentication, the HTTP request must include two headers:

- `X-Rune-Client-Key-Id` \- client key ID
- `X-Rune-Client-Access-Key` \- client access key (secret)

Note that each client may only fetch data for its associated patient.

#### Register a New Client

1. Log in to the [Rune web app](https://app.runelabs.io/) as an admin user.
2. Find the patient you want to access data for, and open **Patient Settings**.
3. Open the **Clients** section.
4. Create a new client. You can name it anything, though typically it is named after your app.
5. A default client key pair will be created with the new client. **Copy** **the key ID _and_ the access key before closing the page**. The access key will never be shown again.

#### Create New Client Keys

You can create any number of client keys for a given client, through the [Rune web app](https://app.runelabs.io/). If you forget a key, you can always create a new key pair and disable the old.

It is highly recommended that you "rotate" keys for a patient every 3-6 months, by creating a new client key, configuring it in the client (e.g. the patient's app), then disabling the old key. You should do this immediately if you believe the key may have been compromised (e.g. stolen from the device, posted over any insecure/public communication channel, etc).

### User Access Tokens

Access tokens are long-lived tokens that belong to a single user of the Rune platform, granting access to any patient in that user's active organization. Only **admin** users have access to the Data API.

To use an access token for authentication, the HTTP request must include the following headers:

- `X-Rune-User-Access-Token-Id` \- access token ID
- `X-Rune-User-Access-Token-Secret` \- access token secret
- `X-Rune-Patient-Id` \- patient ID for the data being accessed

#### Create a New Access Token

1. Log in to the [Rune web app](https://app.runelabs.io/)
2. Click on the profile icon in the top right corner.
3. Click on **User Settings**.
4. On the left sidebar, click on **Access Tokens**.
5. Click **CREATE ACCESS TOKEN**.
6. **Copy the token ID _and_ the token secret before closing the page**. The secret will never be shown again.

It is highly recommended that you "rotate" user access tokens every 3-6 months, by creating a new token and deactivating the old one. Store your access tokens securely, and do not share them.

### JWT

JWTs are generated by the Rune web app upon login. This is not a recommended authentication method for API users.

To use a JWT for authentication, the HTTP request must include two headers:

- `X-Rune-User-Access-Token` \- JWT
- `X-Rune-Patient-Id` \- patient ID for the data being accessed

## Response Format

All endpoints return JSON-formatted data.

## Errors

Standard HTTP status codes are used to indicate success or failure. In the event of most errors, the response body is JSON that includes additional information, with the following format:

```json
{
  "success": false,
  "error": {
    "code": "EnumeratedErrorKind"
    "message": "Human-readable error description",
    "should_retry": false
  }
}
```

# Dataset Upload

Endpoints to initiate a dataset upload or check on the status of an existing dataset.

## Initiate Dataset Upload

Initiate uploading a dataset to the Rune platform.

A dataset may consist of one or multiple files. The `schema_id` is used to determine how many files each dataset contains. See [Key Concepts](https://docs.runelabs.io/data/#section/Overview/Key-Concepts) for details about dataset schemas.

For a dataset upload to be considered complete, **all files** expected by a dataset schema must be uploaded. An empty file may be used as a placeholder.

A successful request contains an "upload_urls" key, with a set of **pre-signed URLs**: one for each file in the requested schema.

To upload each file in the dataset, `POST` a Multipart-encoded file to the corresponding pre-signed "url". This request must include form fields, with the values of the "fields" key.

In addition to the pre-signed URLs, the response also contains:

- The maximum size for each individual file (by default, 2GiB).
- An expiration time (by default, 3h). After this time, uploads will not be accepted to the pre-signed URLs. If the URLs expire, the upload can be retried by making a new request to the endpoint, with the same inputs.

Please contact Rune Support if you need an exception to either of these constraints.

To check the status of an existing dataset upload, use the [Dataset Status](https://docs.runelabs.io/data/#tag/dataset_upload/paths/~1rest~1v1~1dataset~1%7Bdataset_id%7D/get) endpoint.

### Query Parameters

|     |     |
| --- | --- |
| device_id<br>required | string<br>Example:device_id=wwxBBbbD<br>ID of the patient device that recorded this dataset. |
| schema_id<br>required | string<br>Example:schema_id=medtronic-percept-1<br>Dataset schema ID. See [Key Concepts](https://docs.runelabs.io/data/#section/Overview/Key-Concepts) for details about valid schema IDs. |
| session_id<br>required | string[1 .. 512 characters]<br>A string that uniquely identifies the recording session. Requests are considered duplicate if they have the same `device_id`, `session_id`, and `schema_id`.<br>Duplicate requests will return new upload URLs if the upload is not complete. Once the upload is complete, however, a duplicate request will result in an error. |
| start_time | float<br>Optional Unix timestamp, when data collection began. This is only used for record-keeping; it is not validated or used during data parsing. |
| end_time | float<br>Optional Unix timestamp, when data collection ended. This is only used for record-keeping; it is not validated or used during data parsing. |
| tags | any<br>A map of key-value pairs describing the dataset as a whole.<br>These values will be stored along with the raw data and used to annotate it for future use.<br>Tags should not be used to store configuration relevant to parsing or interpreting the dataset: that should be included in the file contents.<br>Tag keys are strings ([1..128] characters). Tag values may be numeric or strings ([1..1024] characters). |

### Responses

**200**
A dataset for this `device_id`, `schema_id`, and `session_id` existed, but the upload was not completed. The response contains new upload URLs.

**201**
A new dataset has been created.

**400**
Bad request

**401**
Not authorized to perform the request

**404**
Not found

**409**
A dataset has already been successfully uploaded for this combination of `device_id`, `schema_id` and `session_id`.

**500**
Internal server error

## Dataset Status

Get the status of a dataset upload.

Once a dataset upload is complete, the status is only visible for 24h after the last file was uploaded. After this time, attempting to fetch the status of the dataset will result in a `404` response.

### Path Parameters

|     |     |
| --- | --- |
| dataset_id<br>required | string<br>Example:37a2a3dc70c825d9ed1dc5bfcc3137b7c63c84f42c89b075975fb99bfb81c105<br>ID of the dataset |

### Responses

**200**
Successful request

**400**
Bad request

**401**
Not authorized to perform the request

**404**
Not found

**500**
Internal server error

## Upload Dataset In a Single Request

Upload all data for a dataset in a single request (AKA "single-shot dataset upload").

For a dataset upload to be considered complete, **all files** expected by a dataset schema must be uploaded. An empty file may be used as a placeholder.

A successful request contains at least the schema ID, the device ID, and the file contents for each file (one for each "stream" associated with the schema). The file contents should be base64 encoded.

### Request Body schema: application/json

|     |     |
| --- | --- |
| device_id<br>required | string<br>ID of the patient device that recorded this stream data. |
| schema_id<br>required | string<br>Dataset schema ID. See [Key Concepts](https://docs.runelabs.io/data/#section/Overview/Key-Concepts) for details about valid schema IDs. |
| stream_data<br>required | object<br>File contents for each for each of the streams in the dataset schema.<br>Keys are the stream name and values are the base64-encoded contents of the file. |
| start_time | float<br>Optional Unix timestamp, when data collection began. This is only used for record-keeping; it is not validated or used during data parsing. |
| end_time | float<br>Optional Unix timestamp, when data collection ended. This is only used for record-keeping; it is not validated or used during data parsing. |
| tags | object<br>A map of key-value pairs describing the dataset as a whole.<br>These values will be stored along with the raw data and used to annotate it for future use.<br>Tags should not be used to store configuration relevant to parsing or interpreting the dataset: that should be included in the file contents.<br>Tag keys are strings ([1..128] characters). Tag values may be numeric or strings ([1..1024] characters). |

### Responses

**201**
A new dataset has been created.

**400**
Bad request

**401**
Not authorized to perform the request

**404**
Not found

**409**
A dataset has already been successfully uploaded for this combination of `device_id`, `schema_id` and `session_id`.

**500**
Internal server error

## Events and Spans

Create an **event** or **span**, which can later be queried via the [Stream API](https://docs.runelabs.io/stream/v2).

These endpoints only accept Client Key authentication.

Events and spans cannot be deleted. However, they will be **overwritten**, if the following values are identical:

- Patient ID (determined via authentication credentials)
- Device ID
- All timestamps (1 for events; 4 for spans)
- Classification

### Create an Event

An **event** is an item that occurs at a moment in time, which is associated with a payload.

### Request Body schema: application/json

|     |     |
| --- | --- |
| time<br>required | float<br>Unix timestamp when the event occurred. |
| classification<br>required | string<br>A string with 2 or 3 period-delimited elements, in the form `<namespace>.<type>` or `<namespace>.<type>.<enum>`.<br>The three elements form a hierarchical classification:<br>- `namespace`\- the general source of the event. This must be one of:<br>  - `clinical` \- a clinical measure, e.g. a UPDRS result<br>  - `device` \- an event generated/detected by the device or sensor itself<br>  - `patient` \- an event reported by the patient<br>- `type` \- the classification of the event, within the namespace.<br>- `enum` ( _optional_) \- a specific enumeration within the type. |
| created_time | float<br>Time that the event was created. If this is present, it is added to the payload, under the key "created_time". |
| ongoing | bool<br>Indicates if the event continues in time indefinitely. Typically, this is used for patient-reported items (e.g. if they log that they are currently experiencing a symptom). When this is `true`, "ongoing": true is set in the payload of the event.<br>For an item that lasts for a finite duration of time, create a **span** (which has a start and end time). |
| device_id<br>required | string<br>ID of the patient device that recorded this event. |
| device_version<br>required | string<br>A string that describes the version of the device that recorded the event (e.g. the Strive iOS app version).<br>This value is not currently used, though it may be in the future. |
| payload<br>required | object<br>A map to associate with the event. There is no validation. |

### Responses

**200**
Successful request.

**400**
Bad request

**401**
Not authorized to perform the request

**404**
Not found

**413**
Payload too large. The entire request body must be at most 512 KiB.

**500**
Internal server error

### Create a Span

A **span** is a special type of event, capturing something that occurred over a **range** of time. Like an event, each span is associated with a payload.

### Request Body schema: application/json

|     |     |
| --- | --- |
| start_time_min | float<br>Unix timestamp; the earliest time when the span could have started. |
| start_time<br>required | float<br>Unix timestamp, when the span started (with high confidence). |
| end_time<br>required | float<br>Unix timestamp, when the span ended (with high confidence). |
| end_time_max | float<br>Unix timestamp; the latest time when the span could have ended. |
| classification<br>required | string<br>A string with 2 or 3 period-delimited elements, in the form `<namespace>.<type>` or `<namespace>.<type>.<enum>`.<br>The three elements form a hierarchical classification:<br>- `namespace`\- the general source of the span. This must be one of:<br>  - `clinical` \- a clinical measure, e.g. a UPDRS result<br>  - `device` \- a span generated/detected by the device or sensor itself<br>  - `patient` \- a span reported by the patient<br>- `type` \- the classification of the span, within the namespace.<br>- `enum` ( _optional_) \- a specific enumeration within the type. |
| created_time | float<br>Time that the span was created. If this is present, it is added to the payload, under the key "created_time". |
| device_id<br>required | string<br>ID of the patient device that recorded this span. |
| device_version<br>required | string<br>A string that describes the version of the device that recorded the span (e.g. the Strive iOS app version).<br>This value is not currently used, though it may be in the future. |
| payload<br>required | object<br>A map to associate with the span. There is no validation. |

### Responses

**200**
Successful request.

**400**
Bad request

**401**
Not authorized to perform the request

**404**
Not found

**413**
Payload too large. The entire request body must be at most 512 KiB.

**500**
Internal server error

# Patient Upload Summary Statistics

Query patient upload statistics per `schema_id` which include:

- Total bytes of raw data uploaded
- Number datasets uploaded
- Creation time of the latest dataset
- Creation time of the earliest dataset

## Patient Upload Summary Statistics

A successful request returns a JSON object, with a `data` key containing the upload summary statistics per `schema_id` uploaded.

The response can contain all upload summary statistics for a patient, or the upload summary statistics for a patient related to a single `schema_id`.

The `patient_id` is a path param in the url. The endpoint accepts a single query param: `schema_id`.

### Path Parameters

|     |     |
| --- | --- |
| patient_id<br>required | string<br>Example:patient-123456789<br>The ID of the patient. |

### Query Parameters

|     |     |
| --- | --- |
| schema_id | string<br>Example:schema_id=medtronic-percept-1<br>Dataset schema ID. See [Key Concepts](https://docs.runelabs.io/data/#section/Overview/Key-Concepts) for details about valid schema IDs. |

### Responses

**200**
Successful request

**400**
Bad request

**401**
Not authorized to perform the request

**404**
Not found

**500**
Internal server error

### Example Response

```json
{
  "success": true,
  "data": {
    "statistics": {
      "medtronic-percept-1": {
        "bytes_uploaded": 2913131,
        "first_upload_created_at": 1661799353.3680818,
        "last_upload_created_at": 1661799453.3680818,
        "number_of_uploads": 1
      },
      "strivestudy-healthkit-2": {
        "bytes_uploaded": 67694947,
        "first_upload_created_at": 1661799200.3680818,
        "last_upload_created_at": 1661799453.3680818,
        "number_of_uploads": 12
      },
      "strivestudy-applewatch-1": {
        "bytes_uploaded": 94847584,
        "first_upload_created_at": 1661799200.3680818,
        "last_upload_created_at": 1661799453.3680818,
        "number_of_uploads": 21
      }
    }
  }
}
```
