Unofficial Oura client and CLI for Bun. Initial browser authorization, durable single-use refresh-token rotation, paginated API reads, and a separate client for the Oura website's private endpoints.
Not affiliated with Oura. Bring your own Oura account and developer application. No hosted proxy, telemetry, bundled credentials, or stored passwords.
bun add oura-bun@0.1.1
bunx --no-install oura-bun --help
Or clone and run:
git clone https://github.com/jcpsimmons/oura-bun.git
cd oura-bun
bun install --frozen-lockfile
bun src/cli.ts
Requires Bun 1.3 or newer. This is a Bun-native TypeScript package, not a Node.js CLI. GitHub installation also works: bun add github:jcpsimmons/oura-bun.
http://localhost:8765/callback to its redirect URIs.OURA_CLIENT_ID and OURA_CLIENT_SECRET in your environment or a local ignored .env. Never put credentials in command arguments or source control.bunx --no-install oura-bun login. Complete Oura's login and consent in your browser. Use --no-open to open the printed local URL yourself.bunx --no-install oura-bun status
bunx --no-install oura-bun get daily_sleep --start 2026-01-01 --end 2026-01-31
get writes private data as NDJSON to stdout. Redirect it to a private location, not a public CI log. Login and status never print tokens.
Defaults request daily workout spo2 stress heart_health. For the sleep example, login --scopes daily is sufficient. Choose only the scopes you need; additional scopes require consent again. The defaults do not include personal, email, heartrate, tag or session. --redirect must match your registered loopback callback. --state /absolute/private/file selects the credential file (this option is a filesystem path, not OAuth's anti-CSRF state).
Save this as sleep-count.ts after completing CLI login, then run bun sleep-count.ts. It uses the same credential file as the CLI and prints a record count without exposing sleep data.
import { OAuth, OuraClient, FileStore, type Tokens } from 'oura-bun';
import { join } from 'node:path';
import { homedir } from 'node:os';
const auth = new OAuth({
clientId: process.env.OURA_CLIENT_ID!,
clientSecret: process.env.OURA_CLIENT_SECRET!,
tokenUrl: process.env.OURA_TOKEN_URL,
store: new FileStore<Tokens>(join(homedir(), '.config/oura-bun/oauth.tokens.json')),
});
const client = new OuraClient(auth);
let count = 0;
for await (const _row of client.records('daily_sleep', {
start_date: '2026-01-01',
end_date: '2026-01-31',
})) {
count += 1; // Replace with your own private storage.
}
console.log(`Read ${count} daily sleep records.`);
status inspects the local credential file; it does not check whether Oura still accepts the credentials. A successful get confirms API access. See setup, API usage and troubleshooting.
refreshPending marker is written before sending the single-use refresh token. A network timeout or crash with an ambiguous outcome requires login again, rather than silently replaying an invalid token.Store.exclusive using a distributed lock for those environments, and read the latest durable state inside it. Never copy one rotating token into several independent stores.Store for stronger protection.Current Oura authorization redirects to moi.ouraring.com/oauth/v2/ext/oauth-authorize. This package defaults token exchanges to the corresponding verified /oauth/v2/ext/oauth-token. Older Oura docs still list api.ouraring.com/oauth/token. Legacy clients can explicitly set OURA_TOKEN_URL to that endpoint. There is no automatic fallback or token replay across issuers.
The web adapter was derived from Oura's public JavaScript, including its SSO cookie exchange and daily-data requests. It uses an isolated browser context for sign-in; it does not scrape your normal browser's cookies or ask for your password.
# Use installed Chrome, or install Playwright Chromium with: bunx playwright install chromium
bunx --no-install oura-bun web-login --channel chrome
bunx --no-install oura-bun web-daily --start 2026-01-01 --end 2026-01-31
import { OuraWeb } from 'oura-bun/web';
const web = new OuraWeb('/absolute/private/oura.session.json');
await web.login({ channel: 'chrome' }); // user completes Oura login/MFA
const scores = await web.scores('2026-01-01');
const days = await web.daily('2026-01-01', '2026-01-31');
Cookies returned by each request are saved before returning data. An expired session produces AuthRequired; rerun web-login to renew through Oura's own SSO. The browser may reuse still-valid SSO cookies from this package's saved session. The website does not expose a browser refresh-token endpoint in its inspected client, so we do not invent one.
Oura says Oura on the Web will be discontinued later in 2026. Use OAuth for durable integrations. Private endpoint contracts may change. The web adapter intentionally exposes only three inspected read endpoints; it does not provide account deletion, consent changes, or organization writes.
See protocol evidence for endpoint provenance, verification and limitations.
bun install --frozen-lockfile
bun test
bun run typecheck
Tests use synthetic credentials and temporary private stores. They cover refresh concurrency, durability failures, invalid grants, callback state, pagination, retry limits and web cookie renewal. CI needs no credentials.
4 commits
TypeScript
100.0%
Unofficial Oura client and CLI for Bun. Initial browser authorization, durable single-use refresh-token rotation, paginated API reads, and a separate client for the Oura website's private endpoints.
Not affiliated with Oura. Bring your own Oura account and developer application. No hosted proxy, telemetry, bundled credentials, or stored passwords.
bun add oura-bun@0.1.1
bunx --no-install oura-bun --help
Or clone and run:
git clone https://github.com/jcpsimmons/oura-bun.git
cd oura-bun
bun install --frozen-lockfile
bun src/cli.ts
Requires Bun 1.3 or newer. This is a Bun-native TypeScript package, not a Node.js CLI. GitHub installation also works: bun add github:jcpsimmons/oura-bun.
http://localhost:8765/callback to its redirect URIs.OURA_CLIENT_ID and OURA_CLIENT_SECRET in your environment or a local ignored .env. Never put credentials in command arguments or source control.bunx --no-install oura-bun login. Complete Oura's login and consent in your browser. Use --no-open to open the printed local URL yourself.bunx --no-install oura-bun status
bunx --no-install oura-bun get daily_sleep --start 2026-01-01 --end 2026-01-31
get writes private data as NDJSON to stdout. Redirect it to a private location, not a public CI log. Login and status never print tokens.
Defaults request daily workout spo2 stress heart_health. For the sleep example, login --scopes daily is sufficient. Choose only the scopes you need; additional scopes require consent again. The defaults do not include personal, email, heartrate, tag or session. --redirect must match your registered loopback callback. --state /absolute/private/file selects the credential file (this option is a filesystem path, not OAuth's anti-CSRF state).
Save this as sleep-count.ts after completing CLI login, then run bun sleep-count.ts. It uses the same credential file as the CLI and prints a record count without exposing sleep data.
import { OAuth, OuraClient, FileStore, type Tokens } from 'oura-bun';
import { join } from 'node:path';
import { homedir } from 'node:os';
const auth = new OAuth({
clientId: process.env.OURA_CLIENT_ID!,
clientSecret: process.env.OURA_CLIENT_SECRET!,
tokenUrl: process.env.OURA_TOKEN_URL,
store: new FileStore<Tokens>(join(homedir(), '.config/oura-bun/oauth.tokens.json')),
});
const client = new OuraClient(auth);
let count = 0;
for await (const _row of client.records('daily_sleep', {
start_date: '2026-01-01',
end_date: '2026-01-31',
})) {
count += 1; // Replace with your own private storage.
}
console.log(`Read ${count} daily sleep records.`);
status inspects the local credential file; it does not check whether Oura still accepts the credentials. A successful get confirms API access. See setup, API usage and troubleshooting.
refreshPending marker is written before sending the single-use refresh token. A network timeout or crash with an ambiguous outcome requires login again, rather than silently replaying an invalid token.Store.exclusive using a distributed lock for those environments, and read the latest durable state inside it. Never copy one rotating token into several independent stores.Store for stronger protection.Current Oura authorization redirects to moi.ouraring.com/oauth/v2/ext/oauth-authorize. This package defaults token exchanges to the corresponding verified /oauth/v2/ext/oauth-token. Older Oura docs still list api.ouraring.com/oauth/token. Legacy clients can explicitly set OURA_TOKEN_URL to that endpoint. There is no automatic fallback or token replay across issuers.
The web adapter was derived from Oura's public JavaScript, including its SSO cookie exchange and daily-data requests. It uses an isolated browser context for sign-in; it does not scrape your normal browser's cookies or ask for your password.
# Use installed Chrome, or install Playwright Chromium with: bunx playwright install chromium
bunx --no-install oura-bun web-login --channel chrome
bunx --no-install oura-bun web-daily --start 2026-01-01 --end 2026-01-31
import { OuraWeb } from 'oura-bun/web';
const web = new OuraWeb('/absolute/private/oura.session.json');
await web.login({ channel: 'chrome' }); // user completes Oura login/MFA
const scores = await web.scores('2026-01-01');
const days = await web.daily('2026-01-01', '2026-01-31');
Cookies returned by each request are saved before returning data. An expired session produces AuthRequired; rerun web-login to renew through Oura's own SSO. The browser may reuse still-valid SSO cookies from this package's saved session. The website does not expose a browser refresh-token endpoint in its inspected client, so we do not invent one.
Oura says Oura on the Web will be discontinued later in 2026. Use OAuth for durable integrations. Private endpoint contracts may change. The web adapter intentionally exposes only three inspected read endpoints; it does not provide account deletion, consent changes, or organization writes.
See protocol evidence for endpoint provenance, verification and limitations.
bun install --frozen-lockfile
bun test
bun run typecheck
Tests use synthetic credentials and temporary private stores. They cover refresh concurrency, durability failures, invalid grants, callback state, pagination, retry limits and web cookie renewal. CI needs no credentials.
4 commits
TypeScript
100.0%