# Intro

#### Welcome to the Pulsoid API documentation.

Pulsoid enables real-time heart rate data transmission from the peripherals(like BLE heart rate monitors, smartwatches, etc) to the **Client**s. Pulsoid API allows reading real-time heart rate data.

**User**s - are real people who stream their heart rate data via Pulsoid.

**Client**s - are applications that want to read Pulsoid users' data. The developer should create client credentials at [pulsoid.net/ui/api-clients](https://pulsoid.net/ui/api-clients). Received client credentials include client\_secret which should be stored securely and never shared.

To consumer Pulsoid API, the developer should obtain an **authorization token**. **An authorization token** is used in every API call. Obtaining the token can be challenging but we are [here](https://discord.com/invite/g9Y4Bp7YYz) to help you :).

#### How to Obtain an Authorization Token

* **For Personal Use**: [Manual Token Issuing](/access-token-management/manual-token-issuing) is suitable and client credentials are not needed.
* **For Websites**: The [Implicit Grant](/access-token-management/oauth2-implicit-grant) is suitable.
* **For Desktop Applications**: If handling deep links, the [Implicit Grant](/access-token-management/oauth2-implicit-grant) is suitable.
* **For Backend Servers**: The [Authorization Code Grant](/access-token-management/oauth2-authorization-code-grant) is recommended.
* **For Plugins or Desktop Applications**: Use [OAuth 2 Device Authorization Flow](/access-token-management/oauth-2-device-authorization-flow).
* **For Plugins or Desktop Applications**: In case [OAuth 2 Device Authorization Flow](/access-token-management/oauth-2-device-authorization-flow) .
* **For Enterprise Applications**: To use Pulsoid for real-time heart rate data, contact us at <support@pulsoid.net> or on [Discord](https://discord.gg/tZktPS5).
* If you still have questions, ask them via <support@pulsoid.net> or on [Discord](https://discord.gg/tZktPS5).

#### JavaScript / TypeScript

If you are building a JavaScript or TypeScript application, use the official [`@pulsoid/socket`](https://www.npmjs.com/package/@pulsoid/socket) library for real-time heart rate streaming. It handles WebSocket connections, auto-reconnection, and provides full TypeScript support with zero dependencies.

```bash
npm install @pulsoid/socket
```

```javascript
import PulsoidSocket from '@pulsoid/socket';

const socket = PulsoidSocket.create('YOUR_TOKEN');

socket.on('heart-rate', (data) => {
  console.log(`${data.heartRate} BPM`);
});

await socket.connect();
```

See the [GitHub repository](https://github.com/pulsoid-oss/pulsoid-socket) for full documentation.

#### Recap

So, to integrate with Pulsoid you need to:

1. Create client credentials at [pulsoid.net/ui/api-clients](https://pulsoid.net/ui/api-clients)
2. Decide, what way of obtaining tokens is suitable for you
3. Use the API

#### FAQ


# Access Token Management


# OAuth2 Implicit Grant

[“Implicit Grant” in the OAuth2 RFC](https://datatracker.ietf.org/doc/html/rfc6749#section-4.2)

1. Send the user you want to authenticate to your registered redirect URI. An authorization page will ask the user to sign up or log into Pulsoid and allow the user to choose whether to authorize your application/identity system.

Create a `<a href="">login</a>`:

```bash
GET https://pulsoid.net/oauth2/authorize
    ?client_id=<your client ID>
    &redirect_uri=<your registered redirect URI>
    &response_type=token
    &scope=<space-separated list of scopes>
    &state=<unique token, generated by your application>
```

Parameters explained:

<table><thead><tr><th width="198">Name</th><th width="118.33333333333331">Type</th><th>Description</th></tr></thead><tbody><tr><td>client_id</td><td>string</td><td>Your client ID.</td></tr><tr><td>redirect_uri</td><td>string</td><td>Your registered redirect URI. This must exactly match the redirect URI registered in the prior.</td></tr><tr><td>response_type</td><td>string</td><td>Should be always <code>token</code></td></tr><tr><td>scope</td><td>string</td><td>Comma-separated list of scopes.</td></tr><tr><td>state</td><td>string</td><td>Your unique token, generated by your application. This is an OAuth 2.0 opaque value, used to avoid CSRF attacks. This value is echoed back in the response.</td></tr></tbody></table>

In our example, you request access to read heart rate data and send the user to <http://localhost>

```bash
GET 'https://pulsoid.net/oauth2/authorize?response_type=token&client_id=3d3fa070-8358-4984-ae32-94392185df63&redirect_uri=http://localhost&scope=data:heart_rate:read&state=a52beaeb-c491-4cd3-b915-16fed71e17a8'
```

2. If the user authorizes your application, the user is redirected to your redirect URL:

```bash
https://<your registered redirect URI>/#token=token_type=bearer&access_token=<access token>&expires_in=90000&scope=data:heart_rate:read&state=<echoed back state your application path on authorization step> 
```

3. After redirecting the application developer can access access\_token from the fragment of the page's URL. [Validate authorization token.](/access-token-management/validate-authorization-token)

[How To Validate Authorization Token?](/access-token-management/validate-authorization-token)


# Copy of OAuth2 Implicit Grant

[“Implicit Grant” in the OAuth2 RFC](https://datatracker.ietf.org/doc/html/rfc6749#section-4.2)

1. Send the user you want to authenticate to your registered redirect URI. An authorization page will ask the user to sign up or log into Pulsoid and allow the user to choose whether to authorize your application/identity system.

Create a `<a href="">login</a>`:

```bash
GET https://pulsoid.net/oauth2/authorize
    ?client_id=<your client ID>
    &redirect_uri=<your registered redirect URI>
    &response_type=token
    &scope=<space-separated list of scopes>
    &state=<unique token, generated by your application>
```

Parameters explained:

<table><thead><tr><th width="198">Name</th><th width="118.33333333333331">Type</th><th>Description</th></tr></thead><tbody><tr><td>client_id</td><td>string</td><td>Your client ID.</td></tr><tr><td>redirect_uri</td><td>string</td><td>Your registered redirect URI. This must exactly match the redirect URI registered in the prior.</td></tr><tr><td>response_type</td><td>string</td><td>Should be always <code>token</code></td></tr><tr><td>scope</td><td>string</td><td>Comma-separated list of scopes.</td></tr><tr><td>state</td><td>string</td><td>Your unique token, generated by your application. This is an OAuth 2.0 opaque value, used to avoid CSRF attacks. This value is echoed back in the response.</td></tr></tbody></table>

In our example, you request access to read heart rate data and send the user to <http://localhost>

```bash
GET 'https://pulsoid.net/oauth2/authorize?response_type=token&client_id=3d3fa070-8358-4984-ae32-94392185df63&redirect_uri=http://localhost&scope=data:heart_rate:read&state=a52beaeb-c491-4cd3-b915-16fed71e17a8'
```

2. If the user authorizes your application, the user is redirected to your redirect URL:

```bash
https://<your registered redirect URI>/#token=token_type=bearer&access_token=<access token>&expires_in=90000&scope=data:heart_rate:read&state=<echoed back state your application path on authorization step> 
```

3. After redirecting the application developer can access access\_token from the fragment of the page's URL. [Validate authorization token.](/access-token-management/validate-authorization-token)

### Response mode

To give more flexibilities we in Pulsoid decided to extend the OAuth2 protocol.

#### Web page response mode

Web page response mode is suitable for mod developers. After authorizing access user will be redirected to the Pulsoid web page with the authorization token. The user can manually copy the authorization token, paste it into the config file, etc.

To enable this capability to add `response_mode=web_page` query parameter from step 1)\
[Implicit Grant.](/access-token-management/oauth2-implicit-grant)

Example:

```bash
GET https://pulsoid.net/oauth2/authorize
    ?client_id=<your client ID>
    &redirect_uri=<your registered redirect URI>
    &response_type=token
    &scope=<space-separated list of scopes>
    &state=<unique token, generated by your application>
    &response_mode=web_page
```

![response mode webpage sample](https://pulsoid-magi.nyc3.cdn.digitaloceanspaces.com/api-documentation/response-mode-webpage-sample.png)

[How To Validate Authorization Token?](/access-token-management/validate-authorization-token)


# OAuth2 Authorization Code Grant

NOTE: Authroization Code Grant Type flow requires trusted server. [“Authorization Code Grant” in the OAuth2 RFC](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1)

1. Send the user you want to authenticate to your registered redirect URI. An authorization page will ask the user to sign up or log into Pulsoid and allow the user to choose whether to authorize your application/identity system.

Create a `<a href="">login</a>`:

```bash
GET https://pulsoid.net/oauth2/authorize
    ?client_id=<your client ID>
    &redirect_uri=<your registered redirect URI>
    &response_type=code
    &scope=<space-separated list of scopes>
    &state=<unique token, generated by your application>
```

Parameters explained:

<table><thead><tr><th width="190.33333333333331">Name</th><th width="181">Type</th><th>Description</th></tr></thead><tbody><tr><td>client_id</td><td>string</td><td>Your client ID.</td></tr><tr><td>redirect_uri</td><td>string</td><td>Your registered redirect URI. This must exactly match the redirect URI registered in the prior.</td></tr><tr><td>response_type</td><td>string</td><td>Should be always <code>code</code></td></tr><tr><td>scope</td><td>string</td><td>Comma-separated list of scopes.</td></tr><tr><td>state</td><td>string</td><td>Your unique token, generated by your application. This is an OAuth 2.0 opaque value, used to avoid CSRF attacks. This value is echoed back in the response.</td></tr></tbody></table>

In our example, you request access to read heart rate data and send the user to <http://localhost>:

```bash
GET 'https://pulsoid.net/oauth2/authorize?response_type=code&client_id=3d3fa070-8358-4984-ae32-94392185df63&redirect_uri=http://localhost&scope=data:heart_rate:read&state=a52beaeb-c491-4cd3-b915-16fed71e17a8'
```

2. If the user authorizes your application, the user is redirected to your redirect URL:

```url
https://<your registered redirect URI>/?code=<authorization code>&state=<echoed back state your application path on authorization step>
```

The OAuth 2.0 authorization code is a randomly generated string. It is used in the next step, a request made to the token endpoint in exchange for an access token. In our example, your user gets redirected to:

```bash
http://localhost/?code=fedc8790-df28-4928-9dcf-55a4d7aa1f5e
    &state=a52beaeb-c491-4cd3-b915-16fed71e17a8
```

3. On your server, get an access token by making this request:

```bash
POST https://pulsoid.net/oauth2/token
Content-Type: application/x-www-form-urlencoded

client_id=<your client ID>
&client_secret=<your client secret>
&code=<authorization code received above>
&grant_type=authorization_code
&redirect_uri=<your registered redirect URI>
```

Here is a sample request:

```bash
POST https://pulsoid.net/oauth2/token
Content-Type: application/x-www-form-urlencoded   

grant_type=authorization_code 
&code=fedc8790-df28-4928-9dcf-55a4d7aa1f5e
&client_id=3d3fa070-8358-4984-ae32-94392185df63
&client_secret=a8262283-f568-4ec3-be84-1c4758dc1a82
&redirect_uri=http://localhost
```

4. We respond with a JSON-encoded access token. The response looks like this:

```bash
{
  "access_token": "<user access token>",
  "refresh_token": "<refresh token>",
  "expires_in": <number of seconds until the token expires>,
  "token_type": "bearer"
}
```

In our example:

```bash
{
  "access_token": "17ebb971-f558-48f2-81b1-788ea927c509",
  "refresh_token": "c6f30bc4-9a04-4e66-a1a1-080fad703a9e",
  "expires_in": 3600,
  "token_type": "bearer"
}
```

Note that code can be exchanged for an access token only once.

[How To Refresh Authorization Token?](/access-token-management/oauth2-refreshing-the-token)

[How To Validate Authorization Token?](/access-token-management/validate-authorization-token)


# OAuth2 Client Credentials Grant Type(server token)

The client credentials grant flow is meant only for server-to-server API requests that use an app access token.

To get an access token, send an HTTP POST request to `https://pulsoid.net/oauth2/token`. Set the following `x-www-form-urlencoded` parameters as appropriate for your app.

<table><thead><tr><th width="177.33333333333331">Name</th><th width="143">Type</th><th>Description</th></tr></thead><tbody><tr><td>grant_type</td><td>string</td><td>Must be set to client_credentials</td></tr></tbody></table>

There should be an Authorization header with base64 encoded client\_id:client\_secret. For example: for\
`client_id=c0403af6-998a-4b22-b08b-cc7329bbdc03` and\
`client_secret=b2729cb2-a34a-4641-aa92-b66b2d27eabd`\
there should be a header

```
Authorization: Basic YzA0MDNhZjYtOTk4YS00YjIyLWIwOGItY2M3MzI5YmJkYzAzOmIyNzI5Y2IyLWEzNGEtNDY0MS1hYTkyLWI2NmIyZDI3ZWFiZA==
```

#### Curl request example

```sh
curl --request POST \
  --url https://pulsoid.net/oauth2/token \
  --header 'Authorization: Basic YzA0MDNhZjYtOTk4YS00YjIyLWIwOGItY2M3MzI5YmJkYzAzOmIyNzI5Y2IyLWEzNGEtNDY0MS1hYTkyLWI2NmIyZDI3ZWFiZA==' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data grant_type=client_credentials \
  --data scope=profile:create
```

#### Curl response example

If the request succeeds, it returns an access token.

```json
{
  "access_token": "4edb0b67-79f9-49ef-ab8f-afb05f24c4d1",
  "expires_in": 5011271,
  "token_type": "bearer"
}
```


# Manual Token Issuing

Client credentials are not required if you use Manual Token authorization.

To obtain a token go to <https://pulsoid.net/ui/keys> and click the button "Create new token".

**Note** that token issuing is a BRO plan feature available under paid subscription or trial.

[Validate access token.](https://docs.pulsoid.net/access-token-management/validate-authorization-token)


# OAuth 2 Device Authorization Flow

**Authorization Token Retrieval Method for Applications Without HTTP or Deep Link Callbacks**

This method is ideal for applications that can't process HTTP or deep link callbacks. It involves multiple stages:

1. Obtain the authorization URL for the user.
2. Open the browser for the user, or instruct them to open it using the provided link.
3. While the user is granting access, the application should poll for the token in the background.
4. After the user grants access, the application will receive the OAuth token.

This approach is effective for games, mods, and other applications that cannot handle deep link redirects.

#### Initiate Device Authorization Session

To get an access token, send an HTTP POST request to `https://pulsoid.net/oauth2/device_authorization`. Set the following `x-www-form-urlencoded` parameters:

* `client_id` equals to your application's client id
* `scope` equals to desired comma separate [scope](/access-token-management/list-of-supported-scopes).

Example request

```sh
curl --request POST \
  --url https://pulsoid.net/oauth2/device_authorization \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data client_id=ad9e3778-347f-4aec-9ec6-98b0d353f6f9 \
  --data 'scope=data:heart_rate:read'
```

Example response

```json
{
  "device_code": "2c169147-c761-4691-accd-00c702cf3b60",
  "user_code": "32761c50-bc77-422d-b1d8-dadae4b9ef37",
  "verification_uri": "https://pulsoid.net/oauth2/device_authorization_consent",
  "verification_uri_complete": "https://pulsoid.net/oauth2/device_authorization_consent?user_code=32761c50-bc77-422d-b1d8-dadae4b9ef37",
  "expires_in": 600,
  "interval": 3
}
```

Your application should open the URL located in `verification_uri_complete` or prompt the user to open this URL. On this page, the user will be able to grant or deny access.

Concurrently, the application should run a background process that attempts to obtain an access token using the `device_code` at the specified `interval`(in seconds), continuing until the `expires_in`(in seconds) duration has elapsed.

#### Obtain Token

To obtain an access token, the application should execute an HTTP POST request to `https://pulsoid.net/oauth2/token`. Set the following `x-www-form-urlencoded` parameters:

* `grant_type` equals `urn:ietf:params:oauth:grant-type:device_code`
* `device_code` equals the one from the previous step
* `client_id` equals your application's client ID

Example Request

```sh
curl --request POST \
  --url https://pulsoid.net/oauth2/token \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data 'grant_type="urn:ietf:params:oauth:grant-type:device_code"' \
  --data device_code=2c169147-c761-4691-accd-00c702cf3b60 \
  --data client_id=ad9e3778-347f-4aec-9ec6-98b0d353f6f9
```

Result to this request will describe what to do next

<mark style="color:$primary;">**200 OK response means that granting is successful**</mark>

Example of response

```json
{
  "access_token": "02a61b22-1c7e-477d-98ed-96f4aef7d5ad",
  "expires_in": 1261440000,
  "token_type": "bearer"
}
```

<mark style="color:orange;">400 Bad Request</mark>

The response in this case will be a json in the following format

```json
{
  "error": "some error",
  "error_description": null
}
```

The most important fields here are the `error` fields, which signal the next step. These fields can be one of the following:

* `authorization_pending`: This means your application should <mark style="color:$success;">**keep trying to obtain a token**</mark>.
* `invalid_grant` (with `error_description`: `user didn't grant access`): This indicates that the user didn't grant access to your application, and the polling <mark style="color:red;">**process should be stopped**</mark>.
* `invalid_grant` (with `error_description`: `access token already issued`): This means that a token was already obtained for the given `device_code`, and the <mark style="color:red;">**polling process should be stopped.**</mark>
* `expired_token`: This means the `device_code` has expired, and the <mark style="color:red;">**polling process should be stopped**</mark><mark style="color:red;">.</mark>


# OAuth2 Refreshing the token

New OAuth2 access tokens have expirations. Token-expiration periods vary in length, based on how the token was acquired. Tokens return an <mark style="color:purple;">expires\_in</mark> field indicating how long the token should last. However, you should build your applications in such a way that they are resilient to token authentication failures. In other words, an application capable of refreshing tokens should not need to know how long a token will live. Rather, it should be prepared to deal with the token becoming invalid at any time.

To allow for applications to remain authenticated for long periods in a world of expiring tokens, we allow for sessions to be refreshed, in accordance with the guidelines in [“Refreshing an Access Token” in the OAuth2 RFC](https://tools.ietf.org/html/rfc6749#section-6). Generally, refresh tokens are used to extend the lifetime of a given authorization.

**How to refresh**

To refresh a token, you need a refresh token coming from a body. For example

```bash
{
  "access_token": "17ebb971-f558-48f2-81b1-788ea927c509",
  "refresh_token": "c6f30bc4-9a04-4e66-a1a1-080fad703a9e",
  "expires_in": 3600,
  "token_type": "bearer"
}
```

You also need the `client_id` and `client_secret` used to generate the above refresh token.\
To refresh, use this request:

```sh
POST https://pulsoid.net/oauth2/token
    --data-urlencode
    ?grant_type=refresh_token
    &refresh_token=<your refresh token>
    &client_id=<your client ID>
    &client_secret=<your client secret>
```

Parameters explained:

<table><thead><tr><th>Name</th><th width="127.33333333333331">Type</th><th>Description</th></tr></thead><tbody><tr><td>client_id</td><td>string</td><td>Your client ID.</td></tr><tr><td>grant_type</td><td>string</td><td>Should be <code>refresh_token</code>.</td></tr><tr><td>client_secret</td><td>string</td><td>Your client secret.</td></tr><tr><td>refresh_token</td><td>string</td><td>Refresh token issued to the client.</td></tr></tbody></table>

Example:

```bash
POST https://pulsoid.net/oauth2/token
    --data-urlencode
    ?grant_type=refresh_token
    &refresh_token=c6f30bc4-9a04-4e66-a1a1-080fad703a9e
    &client_id=3d3fa070-8358-4984-ae32-94392185df63
    &client_secret=a8262283-f568-4ec3-be84-1c4758dc1a82
```

Here is a sample response on success. It contains the new access token, refresh token, and scopes associated with the new grant. Your application should then update its record of the refresh token to be the value provided in this response, as the refresh token may change between requests.

```bash
{
  "access_token": "79f4bbad-8894-4a04-9e4c-e36bfa0a9867",
  "refresh_token": "9ae58a4b-651a-41c1-a0fe-d3a50920da9b",
  "expires_in": 3600,
  "token_type": "bearer"
}
```

After refreshing the old refresh token and access token are invalid. When a user disconnects an app, we delete all tokens for that user. Both refresh and access tokens for that user will return <mark style="color:purple;">401 Unauthorized</mark>. We recommend performing a refresh when you receive a <mark style="color:purple;">401 Unauthorized</mark>.

[How To Validate Authorization Token?](/access-token-management/validate-authorization-token)


# Revoke authorization token

#### **Request:**

<table><thead><tr><th width="233">name</th><th>value</th></tr></thead><tbody><tr><td>url</td><td><code>https://dev.pulsoid.net/oauth2/revoke</code></td></tr><tr><td>method</td><td><code>POST</code></td></tr></tbody></table>

#### Body Parameters explained:

| Name  | Type   | Description           |
| ----- | ------ | --------------------- |
| token | string | Obtained access token |

**cURL Request Example:**

```bash
curl --request POST \
  --url https://dev.pulsoid.net/oauth2/revoke \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data token=ac902193-f705-4db6-8132-cb3243520734
```

**Response Example:**

`200 OK`


# Validate authorization token

#### **Request:**

<table><thead><tr><th width="193">name</th><th>value</th></tr></thead><tbody><tr><td>url</td><td><code>https://dev.pulsoid.net/api/v1/token/validate</code></td></tr><tr><td>method</td><td><code>GET</code></td></tr></tbody></table>

#### Response:

|             |                                                                       |
| ----------- | --------------------------------------------------------------------- |
| token       | string                                                                |
| client\_id  | string                                                                |
| expires\_in | number(in seconds)                                                    |
| profile\_id | string (nullable for server tokens)                                   |
| scopes      | [array of strings](/access-token-management/list-of-supported-scopes) |

#### cURL Request Example:

```bash
curl --request GET \
  --url https://dev.pulsoid.net/api/v1/token/validate \
  --header 'Authorization: Bearer 8c4da3ce-7ed7-4a19-a1f1-058498661e45' \
  --header 'Content-Type: application/json' 
```

#### **Response Example:**

```json
{
  "token": "8c4da3ce-7ed7-4a19-a1f1-058498661e45",
  "client_id": "4463b85f-79cd-445f-abee-70a0887f0a85",
  "expires_in": 625608812,
  "profile_id": "4fda99b9-6dc4-4ef4-9fe7-15f64908ad3f",
  "scopes": [
    "data:heart_rate:read",
    "data:heart_rate:write"
  ]
}
```


# List of supported scopes

* `data:heart_rate:read`
* `data:heart_rate:write`
* `profile:read`
* `profile:create` - scope is available for [`client_credentials`](https://github.com/pulsoid-oss/pulsoid-api/wiki/OAuth2-Client-Credentials-Grant-Type\(server-token\)) grant type
* `widgets:read`
* `widgets:update`
* `data:statistics:read` - [link](/read-heart-rate/read-statistics)
* `data:ingestion_pause:read`
* `data:ingestion_pause:write`
* `geometry_dash_mod:configuration:read`
* `geometry_dash_mod:configuration:write`
* `discord:rich_presence:config:read`
* `discord:rich_presence:config:write`
* `data:room:read`


# Read Latest Heart Rate via HTTP

This method allows reading the latest heart rate of the user. Note that most heart rate monitors can measure changes in heart rate once per second, so it is reasonable to query this endpoint once per 500 ms to receive real-time heart rate data.

**Request:**

<table><thead><tr><th width="131">name</th><th>value</th></tr></thead><tbody><tr><td>url</td><td><code>https://dev.pulsoid.net/api/v1/data/heart_rate/latest</code></td></tr><tr><td>method</td><td><code>GET</code></td></tr><tr><td>scope</td><td><code>data:heart_rate:read</code></td></tr></tbody></table>

**Query Parameters explained:**

<table><thead><tr><th width="165">name</th><th>type</th><th width="176">possible values</th><th width="83">default</th><th>description</th></tr></thead><tbody><tr><td>response_mode</td><td>string</td><td><code>json</code>, <code>text_plain_only_heart_rate</code></td><td><code>json</code></td><td>Allow control format of response</td></tr></tbody></table>

**Headers Parameters explained:**

<table><thead><tr><th width="168">name</th><th>value</th><th>description</th></tr></thead><tbody><tr><td>Authorization</td><td>Bearer {access token}</td><td>How to obtains access token<br><a href="https://docs.pulsoid.net/#how-to-obtain-an-authorization-token">https://docs.pulsoid.net/#how-to-obtain-an-authorization-token</a></td></tr></tbody></table>

**Response:**

| name             | type   | description                       |
| ---------------- | ------ | --------------------------------- |
| measured\_at     | number | Unix timestamp in milliseconds    |
| data             | object | Holds metrics data                |
| data.heart\_rate | number | User's latest received heart rate |

**Specific Errors:**

| http status code | reason                                |
| ---------------- | ------------------------------------- |
| 412              | User doesn't have any heart rate data |

**cURL Request Example:**

```bash
curl --request GET \
  --url https://dev.pulsoid.net/api/v1/data/heart_rate/latest \
  --header 'Authorization: Bearer 8c4da3ce-7ed7-4a19-a1f1-058498661e45' \
  --header 'Content-Type: application/json' 
```

**Response Example**

```json
{
  "measured_at": 1625310655000,
  "data": {
    "heart_rate": 40
  }
}
```

**cURL Request Example with response\_mode=text\_plain\_only\_heart\_rate**

```bash
curl --request GET \
  --url 'https://dev.pulsoid.net/api/v1/data/heart_rate/latest?response_mode=text_plain_only_heart_rate' \
  --header 'Authorization: Bearer a433b947-b2ff-4eea-8751-b0542b76897e'
```

**Response Example with response\_mode=text\_plain\_only\_heart\_rate**

```json
129
```

includes header `content-type: text/plain; charset=UTF-8`


# Read Heart Rate via WebSocket

This method allows reading the heart rate of the user in real-time. The WebSocket connection can be interrupted at any point in time, make sure to have retry logic with backoff.

**Request**

<table><thead><tr><th width="197">name</th><th>value</th></tr></thead><tbody><tr><td>url</td><td><code>wss://dev.pulsoid.net/api/v1/data/real_time</code></td></tr><tr><td>scope</td><td><code>data:heart_rate:read</code></td></tr></tbody></table>

**Authentication**

Provide your OAuth2 Bearer token using one of the following methods:

| Method          | Example                                                               |
| --------------- | --------------------------------------------------------------------- |
| Query parameter | `wss://dev.pulsoid.net/api/v1/data/real_time?access_token=YOUR_TOKEN` |
| Header          | `Authorization: Bearer YOUR_TOKEN`                                    |

**Query Parameters explained:**

<table><thead><tr><th width="186">name</th><th width="103">type</th><th>description</th></tr></thead><tbody><tr><td>access_token</td><td>string</td><td>OAuth2 access token (alternative to Authorization header)</td></tr></tbody></table>

**Websocket URL Request Example**

```url
wss://dev.pulsoid.net/api/v1/data/real_time?access_token=8c4da3ce-7ed7-4a19-a1f1-058498661e45
```

**WebSocket Message Example**

```json
{
  "measured_at": 1625310655000,
  "data": {
    "heart_rate": 40
  }
}
```

**Websocket URL Request Example With response\_mode=text\_plain\_only\_heart\_rate**

```uri
wss://dev.pulsoid.net/api/v1/data/real_time?access_token=8c4da3ce-7ed7-4a19-a1f1-058498661e45&response_mode=text_plain_only_heart_rate
```

**WebSocket Message Example With response\_mode=text\_plain\_only\_heart\_rate**

```json
123
```

***

#### JavaScript / TypeScript

For JavaScript and TypeScript applications, use the official [`@pulsoid/socket`](https://www.npmjs.com/package/@pulsoid/socket) library instead of managing WebSocket connections manually. It provides auto-reconnection with exponential backoff, typed events, and zero dependencies.

```bash
npm install @pulsoid/socket
```

```javascript
import PulsoidSocket from '@pulsoid/socket';

const socket = PulsoidSocket.create('YOUR_TOKEN');

socket.on('heart-rate', (data) => {
  console.log(`${data.heartRate} BPM`);
});

await socket.connect();
```

See the [GitHub repository](https://github.com/pulsoid-oss/pulsoid-socket) for full documentation and configuration options.


# Write Heart Rate via HTTP

Submit a heart rate reading for the authenticated user.

**Request**

<table><thead><tr><th width="197">name</th><th>value</th></tr></thead><tbody><tr><td>url</td><td><code>https://dev.pulsoid.net/api/v1/data</code></td></tr><tr><td>method</td><td><code>POST</code></td></tr><tr><td>content-type</td><td><code>application/json</code></td></tr><tr><td>scope</td><td><code>data:heart_rate:write</code></td></tr></tbody></table>

**Authentication**

Provide your OAuth2 Bearer token using one of the following methods:

| Method          | Example                            |
| --------------- | ---------------------------------- |
| Query parameter | `?access_token=YOUR_TOKEN`         |
| Header          | `Authorization: Bearer YOUR_TOKEN` |

**Request Body**

```json
{
  "measured_at": 1709312400000,
  "data": {
    "heart_rate": 75
  }
}
```

| Field            | Type    | Required | Description                                                     |
| ---------------- | ------- | -------- | --------------------------------------------------------------- |
| measured\_at     | number  | Yes      | Unix timestamp in milliseconds when the heart rate was measured |
| data.heart\_rate | integer | Yes      | Heart rate in beats per minute. Must be greater than 0          |

{% hint style="info" %}
Heart rate values of 0 or below are silently ignored and will not be stored.
{% endhint %}

**HTTP Status Codes**

| Status | Description                                                                    |
| ------ | ------------------------------------------------------------------------------ |
| `200`  | Heart rate data accepted                                                       |
| `400`  | Malformed request body                                                         |
| `401`  | Missing or invalid token, or token does not have `data:heart_rate:write` scope |
| `500`  | Unexpected server error (error code 6001)                                      |

**cURL Request Example**

```bash
curl -X POST https://dev.pulsoid.net/api/v1/data \
  -H "Authorization: Bearer 8c4da3ce-7ed7-4a19-a1f1-058498661e45" \
  -H "Content-Type: application/json" \
  -d '{"measured_at": 1709312400000, "data": {"heart_rate": 75}}'
```


# Page


# Read Statistics

This method allows reading the statistics of bpm data.

**Request:**

<table><thead><tr><th width="131">name</th><th>value</th></tr></thead><tbody><tr><td>url</td><td><code>https://dev.pulsoid.net/api/v1/statistics</code></td></tr><tr><td>method</td><td><code>GET</code></td></tr><tr><td>scope</td><td><pre><code>data:statistics:read
</code></pre></td></tr></tbody></table>

**Query Parameters explained:**

<table><thead><tr><th width="150">name</th><th>type</th><th width="176">possible values</th><th width="83">default</th><th>description</th></tr></thead><tbody><tr><td>time_range</td><td>string</td><td><code>24h</code>,<br><code>7d</code>,<code>30d</code></td><td><code>24h</code></td><td>Time range</td></tr></tbody></table>

**Headers Parameters explained:**

<table><thead><tr><th width="151">name</th><th>value</th><th>description</th></tr></thead><tbody><tr><td>Authorization</td><td>Bearer {auth token}</td><td>How to obtains access token<br><a href="https://docs.pulsoid.net/#how-to-obtain-an-authorization-token">https://docs.pulsoid.net/#how-to-obtain-an-authorization-token</a></td></tr></tbody></table>

**Response:**

<table><thead><tr><th width="278">name</th><th>type</th><th>description</th></tr></thead><tbody><tr><td>maximum_beats_per_minute</td><td>number</td><td>Maximum beats per minute for the provided time range</td></tr><tr><td>minimum_beats_per_minute</td><td>number</td><td>Minimum beats per minute for the provided time range</td></tr><tr><td>average_beats_per_minute</td><td>number</td><td>Average beats per minute for the provided time range</td></tr><tr><td>streamed_duration_in_seconds</td><td>number</td><td>Duration in seconds while heart rate was streamed</td></tr><tr><td>calories_burned_in_kcal</td><td>number</td><td>Estimated calories burned for the provided time range</td></tr></tbody></table>

**Specific Errors:**

| http status code | reason                                   |
| ---------------- | ---------------------------------------- |
| 400              | no\_supported\_time\_range\_24h\_7d\_30d |

**cURL Request Example:**

```bash
curl --request GET \
  --url 'https://dev.pulsoid.net/api/v1/statistics?time_range=7d' \
  --header 'Authorization: Bearer 8c4da3ce-7ed7-4a19-a1f1-058498661e45'
```

**Response Example**

```json
{
	"maximum_beats_per_minute": 139,
	"minimum_beats_per_minute": 60,
	"average_beats_per_minute": 79,
	"streamed_duration_in_seconds": 23019,
	"calories_burned_in_kcal": 1072
}
```


# Widget Entity

**Entity Parameters:**

<table><thead><tr><th width="185.33333333333331">name</th><th width="161">type</th><th>description</th></tr></thead><tbody><tr><td>id</td><td>string</td><td>Identifier of the widget</td></tr><tr><td>meta_id</td><td>string</td><td>Meta identifier of the widget, encodes type of the widget</td></tr><tr><td>configuration</td><td>json</td><td>Valid json, encodes widget's configuration. Exact scheme of configuration is derived from the meta id.</td></tr><tr><td>name</td><td>string</td><td>The widget's name</td></tr><tr><td>premium</td><td>boolean</td><td>Whether the widget requires a premium subscription</td></tr></tbody></table>

**Entity Example**

```json
{
  "id": "95680ad2-8d19-44fe-996c-280db1bd6bf1",
  "meta_id": "f0654a12-3a59-4beb-b567-c5febda19c48",
  "configuration": {
    "ranges": [
      {
        "to": 90,
        "from": 60,
        "color": "#04bbafff"
      },
      {
        "to": 100,
        "from": 91,
        "color": "#fdb008ff"
      }
    ],
    "heartColor": "#f72d21ff",
    "heartEnabled": true
  },
  "name": "Some name",
  "premium": false
}
```


# Create Widget

This method allows the creation of the [widget](/widgets-management/widget-entity).

**Request**

<table><thead><tr><th width="213">name</th><th>value</th></tr></thead><tbody><tr><td>url</td><td><code>https://dev.pulsoid.net/api/v1/widgets</code></td></tr><tr><td>method</td><td><code>POST</code></td></tr><tr><td>scope</td><td><code>widgets:update</code></td></tr></tbody></table>

**Body Param** Accepts JSON object with the following fields

<table><thead><tr><th width="164">name</th><th>value</th></tr></thead><tbody><tr><td>configuration</td><td>Json representing new widget's configuration</td></tr><tr><td>name</td><td>Name of the widget</td></tr><tr><td>meta_id</td><td>Currently only basic bpm is supported so the value should be hardcoded to <code>bf6bdf21-10a7-4a28-97ec-a239e6c77f8b</code></td></tr></tbody></table>

**Headers Parameters explained:**

<table><thead><tr><th width="165">name</th><th>value</th><th>description</th></tr></thead><tbody><tr><td>Authorization</td><td>Bearer {auth token}</td><td>How to obtains access token<br><a href="https://docs.pulsoid.net/#how-to-obtain-an-authorization-token">https://docs.pulsoid.net/#how-to-obtain-an-authorization-token</a></td></tr></tbody></table>

**Response**

The response is a created [widget](/widgets-management/widget-entity).

**Curl Request Example**

```bash
curl --request POST \
  --url https://pulsoid.net/api/v1/widgets \
  --header 'Authorization: Bearer 47ee2a9f-624d-4ed3-84db-8104b229f21c' \
  --header 'Content-Type: application/json' \
  --data '{
  "meta_id": "bf6bdf21-10a7-4a28-97ec-a239e6c77f8b",
  "name": "Test",
  "configuration": {
    "heartEnabled": false
  }
}'
```

**Response Example**

```json
{
  "widget": {
    "id": "a9b9768c-b907-40b7-9530-ae515d8df6b0",
    "meta_id": "bf6bdf21-10a7-4a28-97ec-a239e6c77f8b",
    "configuration": {
      "heartEnabled": false
    },
    "name": "Test"
  }
}
```

***


# Read Widget

This method allows reading the information of the user's widgets.

**Request**

<table><thead><tr><th width="167">name</th><th>value</th></tr></thead><tbody><tr><td>url</td><td><code>https://dev.pulsoid.net/api/v1/widgets</code></td></tr><tr><td>method</td><td><code>GET</code></td></tr><tr><td>scope</td><td><code>widgets:read</code></td></tr></tbody></table>

**Headers Parameters explained:**

<table><thead><tr><th width="170">name</th><th>value</th><th>description</th></tr></thead><tbody><tr><td>Authorization</td><td>Bearer {auth token}</td><td>How to obtains access token<br><a href="https://docs.pulsoid.net/#how-to-obtain-an-authorization-token">https://docs.pulsoid.net/#how-to-obtain-an-authorization-token</a></td></tr></tbody></table>

**Response**

The response is an array of objects. Each object represents a [widget](/widgets-management/widget-entity).

**Curl Request Example**

```bash
curl --request GET \
  --url https://pulsoid.net/api/v1/widgets \
  --header 'Authorization: Bearer 8c4da3ce-7ed7-4a19-a1f1-058498661e45'
```

**Response Example**

```json
{
  "widgets": [
    {
      "id": "40b0131e-fe68-4020-90a0-1a5eb78fd9b4",
      "meta_id": "b828337f-cdb0-49d2-8e55-9b1285cd0d22",
      "configuration": {},
      "name": "Some name"
    },
    {
      "id": "95680ad2-8d19-44fe-996c-280db1bd6bf1",
      "meta_id": "f0654a12-3a59-4beb-b567-c5febda19c48",
      "configuration": {
        "ranges": [
          {
            "to": 90,
            "from": 60,
            "color": "#04bbafff"
          },
          {
            "to": 100,
            "from": 91,
            "color": "#fdb008ff"
          }
        ],
        "heartColor": "#f72d21ff",
        "heartEnabled": true
      },
      "name": "Widget 2"
    }
  ]
}
```

***


# Update Widget

This method allows updating the information of the user's [widget](/widgets-management/widget-entity).

**Request**

<table><thead><tr><th width="164">name</th><th>value</th></tr></thead><tbody><tr><td>url</td><td><code>https://dev.pulsoid.net/api/v1/widgets/{widget.id}</code></td></tr><tr><td>method</td><td><code>POST</code></td></tr><tr><td>scope</td><td><code>widgets:update</code></td></tr></tbody></table>

**Body Param** Accepts JSON object with the following field

<table><thead><tr><th width="162">name</th><th>value</th></tr></thead><tbody><tr><td>configuration</td><td>Json representing new widget's configuration</td></tr></tbody></table>

**Headers Parameters explained:**

<table><thead><tr><th width="166">name</th><th>value</th><th>description</th></tr></thead><tbody><tr><td>Authorization</td><td>Bearer {auth token}</td><td>How to obtains access token<br><a href="https://docs.pulsoid.net/#how-to-obtain-an-authorization-token">https://docs.pulsoid.net/#how-to-obtain-an-authorization-token</a></td></tr></tbody></table>

**Response**

The response is an updated [widget](/widgets-management/widget-entity).

**Curl Request Example**

```bash
curl --request POST \
  --url https://pulsoid.net/api/v1/widgets/40b0131e-fe68-4020-90a0-1a5eb78fd9b4 \
  --header 'Authorization: Bearer 8c4da3ce-7ed7-4a19-a1f1-058498661e45' \
  --header 'Content-Type: application/json' \
  --data '{
  "configuration" : {
    "font": "Comics Sans"
  }
}'
```

**Response Example**

```json
{
  "widget": {
    "id": "40b0131e-fe68-4020-90a0-1a5eb78fd9b4",
    "meta_id": "b828337f-cdb0-49d2-8e55-9b1285cd0d22",
    "configuration": {
      "font": "Comics Sans"
    },
    "name": "Widget 1"
  }
}
```


# Read Profile Information

This method allows reading the information of the user. It includes premium status, channel, and login(can be an email).

### Request

<table><thead><tr><th width="225">name</th><th>value</th></tr></thead><tbody><tr><td>url</td><td><code>https://dev.pulsoid.net/api/v1/profile</code></td></tr><tr><td>method</td><td><code>GET</code></td></tr><tr><td>scope</td><td><code>profile:read</code></td></tr></tbody></table>

### Response

<table><thead><tr><th width="191">name</th><th width="152.33333333333331">type</th><th>description</th></tr></thead><tbody><tr><td>channel</td><td>string</td><td>User's channel e.g. twitch.tv/pulsoid</td></tr><tr><td>username</td><td>string</td><td>Username e.g. pulsoid, support@pulsoid.net</td></tr><tr><td>mobile_login</td><td>boolean</td><td>Indicates whether user did login via mobile client</td></tr><tr><td>heart_rate</td><td>boolean</td><td>Indicates whether user transmit any heart rate data</td></tr></tbody></table>

### cURL Request Example

```bash
curl --request GET \
  --url https://pulsoid.net/api/v1/profile \
  --header 'Authorization: Bearer 8c4da3ce-7ed7-4a19-a1f1-058498661e45'
```

### Response Example

```json
{
	"channel": "twitch.tv/pulsoid",
	"username": "support@pulsoid.net",
	"mobile_login": true,
	"heart_rate": true
}
```

***


# Error Code Format

|                |        |            |
| -------------- | ------ | ---------- |
| error\_code    | number | Error code |
| error\_message | string | readable   |

```
{
    "error_code": "7010",
    "error_message": "error_authorization_header_has_wrong_format"
}
```

| error\_code | error\_message                                   | http code |
| ----------- | ------------------------------------------------ | :-------: |
| 7001        | error\_while\_validating\_token                  |    500    |
| 7002        | error\_while\_validating\_token                  |    500    |
| 7003        | error\_while\_validating\_token                  |    500    |
| 7004        | error\_while\_validating\_token                  |    500    |
| 7005        | token\_not\_found                                |    401    |
| 7006        | token\_expired                                   |    401    |
| 7007        | premium\_required                                |    402    |
| 7008        | error\_returning\_result                         |    500    |
| 7009        | error\_authorization\_header\_is\_not\_present   |    403    |
| 7010        | error\_authorization\_header\_has\_wrong\_format |    403    |
| 7011        | error\_invalid\_scope                            |    400    |
| 7012        | error\_processing\_your\_request                 |    500    |
| 7013        | invalid\_request                                 |    400    |
| 7014        | not\_supported\_meta\_id                         |    400    |
| 7015        | no\_supported\_time\_range\_24h\_7d\_30d         |    400    |
| 6001        | error\_while\_validating\_token                  |    500    |
| 6002        | error\_retrieving\_latest\_heart\_rate           |    500    |
| 6003        | error\_invalid\_scope                            |    400    |
| 6004        | error\_invalid\_response\_mode                   |    422    |


# Geometry Dash Mod Configuration Entity

```json
{
    "disable_start": null,
    "enable_death": null,
    "death_bpm": null,
    "scale": null,
    "position": null,
    "custom_x": null,
    "custom_y": null,
    "heart_pos": null,
    "death_pos": null,
    "enable_color_desaturation": null,
    "desaturation_bpm_start": null,
    "desaturation_bpm_end": null
}
```


# Get Configuration

This method allows the reading of geometry dash configuration.

**Request**

<table><thead><tr><th width="213">name</th><th>value</th></tr></thead><tbody><tr><td>url</td><td><pre><code>https://dev.pulsoid.net/api/v1/geometry-dash-mod/configuration
</code></pre></td></tr><tr><td>method</td><td><code>GET</code></td></tr><tr><td>scope</td><td><code>geometry_dash_mod:configuration:read</code></td></tr></tbody></table>

**Headers Parameters explained:**

<table><thead><tr><th width="165">name</th><th>value</th><th>description</th></tr></thead><tbody><tr><td>Authorization</td><td>Bearer {auth token}</td><td>How to obtains access token<br><a href="https://docs.pulsoid.net/#how-to-obtain-an-authorization-token">https://docs.pulsoid.net/#how-to-obtain-an-authorization-token</a></td></tr></tbody></table>

**Response**

Specific Error&#x73;**:**

| http status code | reason                                             |
| ---------------- | -------------------------------------------------- |
| 404              | User doesn't have any geometry dash configurations |

The response body in case 200 http status is a [geometry dash mod configution](/geometry-dash-mod-management/geometry-dash-mod-configuration-entity) .

**Curl Request Example**

```bash
curl --request GET \
  --url https://dev.pulsoid.net/api/v1/geometry-dash-mod/configuration \
  --header 'Authorization: Bearer 47ee2a9f-624d-4ed3-84db-8104b229f21c'
```

**Response Example**

```json
{
  "configuration": {
    "disable_start": true,
    "enable_death": null,
    "death_bpm": 1,
    "scale": null,
    "position": null,
    "custom_x": null,
    "custom_y": null,
    "heart_pos": null,
    "death_pos": null,
    "enable_color_desaturation": null,
    "desaturation_bpm_start": null,
    "desaturation_bpm_end": null
  }
}
```

***


# Update Configuration

This method allows the updating/overwriting of geometry dash configuration.

**Request**

<table><thead><tr><th width="213">name</th><th>value</th></tr></thead><tbody><tr><td>url</td><td><pre><code>https://dev.pulsoid.net/api/v1/geometry-dash-mod/configuration
</code></pre></td></tr><tr><td>method</td><td><code>POST</code></td></tr><tr><td>scope</td><td><code>geometry_dash_mod:configuration:write</code></td></tr><tr><td></td><td></td></tr></tbody></table>

**Headers Parameters explained:**

<table><thead><tr><th width="165">name</th><th>value</th><th>description</th></tr></thead><tbody><tr><td>Authorization</td><td>Bearer {auth token}</td><td>How to obtains access token<br><a href="https://docs.pulsoid.net/#how-to-obtain-an-authorization-token">https://docs.pulsoid.net/#how-to-obtain-an-authorization-token</a></td></tr></tbody></table>

**Request Param**

<table><thead><tr><th>name</th><th width="296.265625">type</th><th>description</th></tr></thead><tbody><tr><td>configuration</td><td><a href="/pages/Oe7aKCjb9NuiLW7ydRcA">geometry dash mod configuration</a></td><td>mod configuration</td></tr></tbody></table>

**NOTE:** This API overwrites the existing configuration, so the client should submit the full configuration every time.

Example of request body

```json
{
  "configuration": {
    "disable_start": true,
    "death_bpm": 1
  }
}
```

#### Response

The response body in case 200 http status is a [geometry dash mod configution](/geometry-dash-mod-management/geometry-dash-mod-configuration-entity) .

**Curl Request Example**

```bash
curl --request POST \
  --url https://dev.pulsoid.net/api/v1/geometry-dash-mod/configuration \
  --header 'Authorization: Bearer 47ee2a9f-624d-4ed3-84db-8104b229f21c' \
  --header 'content-type: application/json' \
  --data '{
  "configuration": {
    "disable_start": true,
    "death_bpm": 1
  }
}'
```

**Response Example**

```json
{
  "configuration": {
    "disable_start": true,
    "enable_death": null,
    "death_bpm": 1,
    "scale": null,
    "position": null,
    "custom_x": null,
    "custom_y": null,
    "heart_pos": null,
    "death_pos": null,
    "enable_color_desaturation": null,
    "desaturation_bpm_start": null,
    "desaturation_bpm_end": null
  }
}
```

***


# Feature Status

## Feature Status

Check whether a particular feature is enabled for the authenticated user.

### Request

| Name   | Value                                                  |
| ------ | ------------------------------------------------------ |
| URL    | `https://dev.pulsoid.net/api/v1/features/{feature_id}` |
| Method | `GET`                                                  |

#### Headers

| Name          | Value                   | Description                                                                                     |
| ------------- | ----------------------- | ----------------------------------------------------------------------------------------------- |
| Authorization | `Bearer {access_token}` | [How to obtain an access token](https://docs.pulsoid.net/#how-to-obtain-an-authorization-token) |

#### Path Parameters

| Name         | Type   | Description                                           |
| ------------ | ------ | ----------------------------------------------------- |
| `feature_id` | string | The feature identifier. See Available Features below. |

### Available Features

| `feature_id`                          | Description                 | Condition                                                                                         |
| ------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------- |
| `gd_mod`                              | Geometry Dash Mod           | Enabled with active premium (subscription, trial, or lifetime). Can also be granted individually. |
| `premium_widgets`                     | Premium Widgets             | Enabled with any active premium plan (subscription, trial, or lifetime).                          |
| `stream_deck_ingestion_pause_feature` | Stream Deck Ingestion Pause | Always enabled for all users.                                                                     |
| `discord_rich_presence`               | Discord Rich Presence       | Enabled with active premium (subscription, trial, or lifetime). Can also be granted individually. |

### Response

{% hint style="info" %}
Returns `404 Not Found` if the `feature_id` is not recognized.
{% endhint %}

```json
{
  "id": "premium_widgets",
  "enabled": true
}
```

| Field     | Type    | Description                                  |
| --------- | ------- | -------------------------------------------- |
| `id`      | string  | The requested feature identifier.            |
| `enabled` | boolean | Whether the feature is enabled for the user. |

### Example

**cURL Request**

```bash
curl --request GET \
  --url https://dev.pulsoid.net/api/v1/features/premium_widgets \
  --header 'Authorization: Bearer 052b0236-eac4-45c7-b889-556ececd4e90'
```

**Response**

```json
{
  "id": "premium_widgets",
  "enabled": true
}
```


# VRChat World Integration

VRChat Worlds x Pulsoid

### High Level Approach

At a high level, Pulsoid exposes heart rate data via the Pulsoid MIDI Protocol. A VRChat world can listen to the beats per minute transmitted through [MIDI](< https://creators.vrchat.com/worlds/udon/midi/>). As a result, the VRChat world can receive and react to this data.

{% embed url="<https://www.youtube.com/watch?v=LH_wfSFVFyo>" %}
demo world
{% endembed %}

We’ve provided [prefabs](https://github.com/pulsoid-oss/pulsoid-vrchat-integration) for Unity, but you’re free to integrate them in your own way.&#x20;

The Demo World showcases the following features:

* Displaying all heart rates on a board
* Changing the floor color based on the combined heart rate
* Showing individual heart rate on the wrist

### Pulsoid MIDI Protocol

The MIDI (Musical Instrument Digital Interface) protocol is a technical standard that enables electronic musical instruments, computers, and other devices to communicate and synchronise with each other by transmitting musical performance data, such as note pitches, timings, and control signals, over a serial connection. It allows for the control of multiple instruments from a single controller, facilitating complex and synchronised musical compositions and performances.

Pulsoid supports transmitting heart rate data via MIDI protocol by Pulsoid MIDI Protocol.

MIDI message consists of 3 bytes:

* message byte
* data 1 byte
* data 2 bytes

#### Message Byte

Message byte is used to define command and channel number. There are 7 different commands and 16 different channels.&#x20;

To transfer heart rate data, the Pulsoid MIDI Protocol uses&#x20;

* **Note ON command**
* **1 channel**

So the format of first(message) byte is always the same.

#### Data Bytes

Each data byte can transfer up to 7 bits of information (1 bit is reserved), which is equal to a number from 0 to 127 inclusive. However, heart rate is typically in the range of 30 to 230.

To transfer a heart rate value over the MIDI protocol, we need to encode the value from the range \[30, 230] into two ranges: \[0, 127] (data byte 1) and \[0, 127] (data byte 2).

To achieve this, we will split the beats per minute (BPM) value into two parts:

• Number of tens

• Number of ones

For example:

• bpm = 134, number of tens = 13, number of ones = 4

• bpm = 98, number of tens = 9, number of ones = 8

Then, the number of tens is transferred as the byte 1 value and the number of ones is transferred as the byte 2 value.

#### Examples

67 bpm transferred by Pulsoid MIDI Protocol

<figure><img src="/files/Ma50OhRFgMliEXioBlxw" alt=""><figcaption><p>67 bpm transferred by Pulsoid MIDI Protocol</p></figcaption></figure>

<figure><img src="/files/KJllWwIsuNq4shizGsWY" alt=""><figcaption><p>Raw bytes for bpm 67</p></figcaption></figure>

* 90 - message type 9(Note on) on the first channel (0)
* 07 - number of ones
* 06 - number of tens

To decode bpm back `6 * 10 + 7 = 67`

### References

* MIDI message format explained [`↗`](https://www.songstuff.com/recording/article/midi-message-format/)
* WebMIDI.js [`↗`](https://webmidijs.org/)
* Github Repo [`↗`](https://github.com/pulsoid-oss/pulsoid-vrchat-integration)&#x20;
* Midi in Udon [`↗`](< https://creators.vrchat.com/worlds/udon/midi/>)&#x20;


# Read Room Data via WebSocket

Rooms group multiple Pulsoid users so their real-time heart rate data and membership changes can be consumed through a single WebSocket connection.

A client connects to a room's WebSocket endpoint and receives a stream of messages: heart rate updates from all room members, membership changes, and room configuration updates.

{% hint style="info" %}
Your application should implement reconnection logic. If the WebSocket connection drops, reconnect after a short delay.
{% endhint %}

***

#### Request

| Key            | Value                                                        |
| -------------- | ------------------------------------------------------------ |
| URL            | `wss://dev.pulsoid.net/api/v2/data/rooms/{roomId}/real_time` |
| Required scope | `data:room:read`                                             |

Replace `{roomId}` with the ID of the room you want to subscribe to. The connecting user must be a member of the room.

#### Authentication

Provide your OAuth2 Bearer token using one of the following methods:

| Method          | Example                                                                              |
| --------------- | ------------------------------------------------------------------------------------ |
| Query parameter | `wss://dev.pulsoid.net/api/v2/data/rooms/{roomId}/real_time?access_token=YOUR_TOKEN` |
| Header          | `Authorization: Bearer YOUR_TOKEN`                                                   |

#### Query Parameters

| Parameter      | Type     | Default      | Description                                                 |
| -------------- | -------- | ------------ | ----------------------------------------------------------- |
| `access_token` | `string` |              | OAuth2 access token (alternative to `Authorization` header) |
| `kinds`        | `string` | `heart_rate` | Comma-separated list of message kinds to subscribe to       |

#### Available Message Kinds

| Kind                  | Description                                    |
| --------------------- | ---------------------------------------------- |
| `heart_rate`          | Real-time heart rate updates from room members |
| `room_member_updated` | A member's profile or config was added/changed |
| `room_member_removed` | A member was removed from the room             |
| `room_updated`        | The room configuration was changed             |

To subscribe to multiple kinds, separate them with commas:

```
?kinds=heart_rate,room_member_updated,room_member_removed,room_updated
```

***

#### Response Messages

All messages are JSON objects with the following base structure:

| Field       | Type     | Description                                   |
| ----------- | -------- | --------------------------------------------- |
| `kind`      | `string` | The message kind (see table above)            |
| `timestamp` | `string` | ISO 8601 timestamp of when the event occurred |

Each message includes exactly one additional payload field matching its `kind`.

**`heart_rate`**

Contains the heart rate reading from a room member.

```json
{
  "kind": "heart_rate",
  "timestamp": "2026-02-21T12:00:00Z",
  "heart_rate": {
    "profile_id": "507f1f77bcf86cd799439011",
    "bpm": 85
  }
}
```

| Field                   | Type      | Description                          |
| ----------------------- | --------- | ------------------------------------ |
| `heart_rate.profile_id` | `string`  | Unique identifier of the room member |
| `heart_rate.bpm`        | `integer` | Heart rate in beats per minute       |

**`room_member_updated`**

Sent when a member is added to the room or their profile/config changes.

```json
{
  "kind": "room_member_updated",
  "timestamp": "2026-02-21T12:00:00Z",
  "room_member_updated": {
    "profile_id": "507f1f77bcf86cd799439011",
    "config": { "color": "#ff0000" }
  }
}
```

| Field                            | Type     | Description                                 |
| -------------------------------- | -------- | ------------------------------------------- |
| `room_member_updated.profile_id` | `string` | Unique identifier of the room member        |
| `room_member_updated.config`     | `object` | Arbitrary configuration data for the member |

**`room_member_removed`**

Sent when a member is removed from the room.

```json
{
  "kind": "room_member_removed",
  "timestamp": "2026-02-21T12:00:00Z",
  "room_member_removed": {
    "profile_id": "507f1f77bcf86cd799439011"
  }
}
```

| Field                            | Type     | Description                             |
| -------------------------------- | -------- | --------------------------------------- |
| `room_member_removed.profile_id` | `string` | Unique identifier of the removed member |

**`room_updated`**

Sent when the room configuration changes.

```json
{
  "kind": "room_updated",
  "timestamp": "2026-02-21T12:00:00Z",
  "room_updated": {
    "room_id": "room-abc-123",
    "config": { "theme": "dark", "layout": "grid" }
  }
}
```

| Field                  | Type     | Description                               |
| ---------------------- | -------- | ----------------------------------------- |
| `room_updated.room_id` | `string` | Identifier of the room                    |
| `room_updated.config`  | `object` | Arbitrary configuration data for the room |

***

#### Lazy Delivery of Initial State

When you first connect, you will **not** immediately receive room and member configuration messages. Instead, the server uses lazy delivery:

1. The room configuration (`room_updated`) is sent **once**, just before the first `heart_rate` message arrives.
2. Each member's configuration (`room_member_updated`) is sent **once**, when that member's first heart rate appears.

This means no messages arrive until heart rate data starts flowing, unless an explicit room or member update is triggered server-side.

{% hint style="warning" %}
If you need room and member configuration on connect, subscribe to `room_updated` and `room_member_updated` kinds. The configs will arrive automatically before the first heart rate data.
{% endhint %}

***

#### HTTP Status Codes

| Status | Description                                                                 |
| ------ | --------------------------------------------------------------------------- |
| `101`  | WebSocket upgrade successful                                                |
| `401`  | Missing or invalid token, or token does not have the `data:room:read` scope |
| `403`  | The authenticated user is not a member of the room                          |
| `500`  | Unexpected server error                                                     |

***

#### Connection Lifecycle

* The WebSocket connection will **automatically close** when the OAuth2 token expires.
* If a member is **removed from the room** while connected, their WebSocket connection is disconnected.
* The server sends only **text messages** (JSON). The client should ignore any incoming binary frames.

***

#### Example: JavaScript with `@pulsoid/socket`

For JavaScript and TypeScript applications, use the official [`@pulsoid/socket`](https://www.npmjs.com/package/@pulsoid/socket) library. It handles auto-reconnection, provides typed events, and has zero dependencies.

```bash
npm install @pulsoid/socket
```

```javascript
import PulsoidSocket from '@pulsoid/socket';

const room = PulsoidSocket.createRoom('YOUR_TOKEN', 'your-room-id');

room.on('heart-rate', (data) => {
  console.log(`${data.profileId}: ${data.bpm} BPM`);
});

room.on('room-member-updated', (data) => {
  console.log(`Member updated: ${data.profileId}`);
});

room.on('room-member-removed', (data) => {
  console.log(`Member removed: ${data.profileId}`);
});

room.on('room-updated', (data) => {
  console.log('Room config updated');
});

await room.connect();
```

See the [GitHub repository](https://github.com/pulsoid-oss/pulsoid-socket) for full documentation and configuration options.

#### Example: JavaScript with native WebSocket

```javascript
const roomId = "your-room-id";
const token = "your-access-token";
const kinds = "heart_rate,room_member_updated,room_member_removed,room_updated";

const url = `wss://dev.pulsoid.net/api/v2/data/rooms/${roomId}/real_time?access_token=${token}&kinds=${kinds}`;

const ws = new WebSocket(url);

ws.onopen = () => {
  console.log("Connected to room");
};

ws.onmessage = (event) => {
  const message = JSON.parse(event.data);

  switch (message.kind) {
    case "heart_rate":
      console.log(`${message.heart_rate.profile_id}: ${message.heart_rate.bpm} BPM`);
      break;
    case "room_member_updated":
      console.log(`Member updated: ${message.room_member_updated.profile_id}`);
      break;
    case "room_member_removed":
      console.log(`Member removed: ${message.room_member_removed.profile_id}`);
      break;
    case "room_updated":
      console.log(`Room config updated`);
      break;
  }
};

ws.onclose = (event) => {
  console.log("Disconnected from room", event.code, event.reason);
  // Implement reconnection logic here
};

ws.onerror = (error) => {
  console.error("WebSocket error:", error);
};
```


