Examples
Authentication
All requests must include your API key in the X-API-KEY header. There is no token exchange; include the key directly.
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 -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.
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:
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).
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")
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.
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")
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.
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
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}")