Skip to content

Examples

Authentication

All requests must include your API key in the X-API-KEY header. There is no token exchange; include the key directly.

Python
import requests

headers = {
    "X-API-KEY": "YOUR_API_KEY",
}

response = requests.get(
    "https://research-api.nowatch.com/v1/user/YOUR_USER_ID",
    headers=headers,
)
response.raise_for_status()
print(response.json())
cURL
curl -X GET "https://research-api.nowatch.com/v1/user/YOUR_USER_ID" \
  -H "X-API-KEY: YOUR_API_KEY"

Encoding

Bulk endpoints (application/octet-stream) return Apache Parquet, which is the most efficient format for large date ranges.

Measurement Request to DataFrame
import io
import requests
import polars as pl

NW_API_KEY = "YOUR_API_KEY"
TEST_USER_ID = "YOUR_USER_ID"

headers = {
    "X-API-KEY": NW_API_KEY,
    "Accept": "application/octet-stream",
    "Accept-Encoding": "gzip",
}

response = requests.get(
    f"https://research-api.nowatch.com/v1/timeline/timeseries/{TEST_USER_ID}/ACTIVITY_COUNT",
    headers=headers,
    params={"start_date": "2024-04-01", "end_date": "2024-04-07"},
)

if response.status_code == 200:
    df = pl.read_parquet(io.BytesIO(response.content))
    print(f"rows: {len(df)}")
    print(df.head(5))
else:
    raise Exception(f"Response returned non valid status code: {response.status_code}")

Pagination: cursor walk

Range endpoints (timeseries, sleep, activities, check-ins, intentions, feelings, crown presses) return a paginated envelope:

{ "data": ..., "next_cursor": "<token or null>" }

Pass next_cursor back as ?cursor=... to fetch the next page. When next_cursor is null the walk is complete. The days parameter controls how many calendar days are included per page (default 1, max 7).

Heart rate cursor walk
import requests

BASE_URL = "https://research-api.nowatch.com"
headers = {"X-API-KEY": "YOUR_API_KEY"}

all_timestamps, all_values = [], []
url = f"{BASE_URL}/v1/timeline/timeseries/YOUR_USER_ID/HEART_RATE"
params = {"start_date": "2024-04-01", "end_date": "2024-04-07", "days": 1}

while True:
    resp = requests.get(url, headers=headers, params=params)
    resp.raise_for_status()
    page = resp.json()

    data = page["data"]
    all_timestamps.extend(data["timestamp"])
    all_values.extend(data["value"])

    next_cursor = page.get("next_cursor")
    if not next_cursor:
        break
    params = {"cursor": next_cursor}

print(f"Fetched {len(all_timestamps)} rows total")
Feelings cursor walk
import requests
import polars as pl

BASE_URL = "https://research-api.nowatch.com"
headers = {"X-API-KEY": "YOUR_API_KEY"}

all_rows = []
url = f"{BASE_URL}/v1/feelings/YOUR_USER_ID/DAY"
params = {"start_date": "2024-04-01", "end_date": "2024-04-30", "days": 7}

while True:
    resp = requests.get(url, headers=headers, params=params)
    resp.raise_for_status()
    page = resp.json()

    all_rows.extend(page["data"])

    next_cursor = page.get("next_cursor")
    if not next_cursor:
        break
    params = {"cursor": next_cursor}

df = pl.DataFrame(all_rows)
print(df)

Rate limit headers

Every response from a rate-limited endpoint includes headers that tell you how much quota remains. Read them to pace your requests and avoid 429 errors.

Inspect rate-limit headers
import requests

headers = {"X-API-KEY": "YOUR_API_KEY"}

resp = requests.get(
    "https://research-api.nowatch.com/v1/user/YOUR_USER_ID",
    headers=headers,
)

print("Limit:    ", resp.headers.get("X-RateLimit-Limit"))
print("Remaining:", resp.headers.get("X-RateLimit-Remaining"))
print("Reset in: ", resp.headers.get("X-RateLimit-Reset"), "seconds")

if resp.status_code == 429:
    retry_after = int(resp.headers.get("Retry-After", 1))
    print(f"Rate limited; retry after {retry_after}s")
Polite cursor walk respecting rate limits
import time
import requests

BASE_URL = "https://research-api.nowatch.com"
headers = {"X-API-KEY": "YOUR_API_KEY"}

all_rows = []
url = f"{BASE_URL}/v1/timeline/events/sleep/YOUR_USER_ID"
params = {"start_date": "2024-04-01", "end_date": "2024-04-30", "days": 1}

while True:
    resp = requests.get(url, headers=headers, params=params)

    if resp.status_code == 429:
        retry_after = int(resp.headers.get("Retry-After", 1))
        time.sleep(retry_after)
        continue

    resp.raise_for_status()
    page = resp.json()
    all_rows.extend(page["data"])

    # Slow down if quota is nearly exhausted
    remaining = int(resp.headers.get("X-RateLimit-Remaining", 1))
    if remaining <= 1:
        reset_in = int(resp.headers.get("X-RateLimit-Reset", 1))
        time.sleep(reset_in)

    next_cursor = page.get("next_cursor")
    if not next_cursor:
        break
    params = {"cursor": next_cursor}

print(f"Fetched {len(all_rows)} sleep sessions")

Fetching Overview Data

Overview endpoints return a plain list of daily aggregated metrics. There is no pagination.

Overview Data Request
import requests

NW_API_KEY = "YOUR_API_KEY"
TEST_USER_ID = "YOUR_USER_ID"
start_date = "2023-01-01"
end_date = "2023-01-31"
stype = "HRV"  # HRV, SLEEP_DURATION, SLEEP_REGULARITY, INTENSE_ACTIVITY,
               # RHR, STRESS_DURATION, STRESS_RECOVERY, STRESS_FREQUENCY

headers = {
    "X-API-KEY": NW_API_KEY,
    "Accept": "application/json",
}

response = requests.get(
    f"https://research-api.nowatch.com/v1/overview/{TEST_USER_ID}/{stype}",
    headers=headers,
    params={"start_date": start_date, "end_date": end_date},
)

if response.status_code == 200:
    print(response.json())
else:
    print(f"Error: {response.status_code} - {response.text}")

Fetching Feelings Data

Feelings Data Request
import requests

NW_API_KEY = "YOUR_API_KEY"
TEST_USER_ID = "YOUR_USER_ID"
start_date = "2023-01-01"
end_date = "2023-01-31"
ftype = "DAY"  # DAY or NIGHT

headers = {
    "X-API-KEY": NW_API_KEY,
    "Accept": "application/json",
}

response = requests.get(
    f"https://research-api.nowatch.com/v1/feelings/{TEST_USER_ID}/{ftype}",
    headers=headers,
    params={"start_date": start_date, "end_date": end_date},
)

if response.status_code == 200:
    page = response.json()
    print(page["data"])
else:
    print(f"Error: {response.status_code} - {response.text}")