The type-safe SDK for the quantified self.
TypeScript-first, runtime-validated SDK for the exist.io API. Supports Node.js, Deno, and Bun.
| Runtime | Install |
|---|---|
| Node.js | npm install @fromo/exist-sdk |
| Deno / Bun | import from 'jsr:@fromo/exist-sdk' |
Requires Node.js 18+ (or any runtime with native fetch).
import {createClient, exchangeSimpleToken, getProfile} from '@fromo/exist-sdk';
const {token} = await exchangeSimpleToken({
username: process.env.EXIST_USERNAME!,
password: process.env.EXIST_PASSWORD!,
});
const client = createClient({token});
const profile = await getProfile(client);
console.log(profile.username);You could write fetch() calls to the exist.io API. You could manage token auth, response parsing, and error handling yourself. Or you could use a library that does all of that — with type safety, runtime validation, and PKCE-only OAuth2 built in.
exist-sdk validates every API response against a Zod schema at runtime. If exist.io changes a response shape, you'll get a typed error immediately — not a silent undefined or a cryptic data.whatev failure six hours later.
- Full API coverage — Profile, attributes, averages, correlations, and write operations
- Runtime validation — All responses validated with Zod; catches API mismatches before they crash your app
- PKCE-only OAuth2 — Authorization code and device code flows; no insecure fallback
- Branded token types —
ApiTokenandUserTokenare distinct types; TypeScript won't let you mix them - Multi-runtime — Same codebase for Node.js, Deno, and Bun via JSR
- Auto-generated types — Types stay in sync with the API via
openapi-typescript
Best for scripts and CLIs. Exchange credentials once, store the token, reuse it:
const {token} = await exchangeSimpleToken({
username: 'your-username',
password: 'your-password',
});
const client = createClient({token});Or create a client with a token you generated manually at exist.io/account/api/:
const client = createClient({token: 'your-token'});Warning
This SDK only supports OAuth2 with PKCE. Flows without PKCE are not supported. This is intentional — PKCE protects your users' data even if your client's credentials are exposed.
Authorization code flow (for web apps):
import {createOAuth2Client, FileTokenStore} from '@fromo/exist-sdk';
const oauth2 = createOAuth2Client({
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
issuer: new URL('https://exist.io/'),
redirectUri: 'https://your-app.com/callback',
tokenStore: new FileTokenStore('.exist-tokens.json'),
flow: 'authorization_code',
});
// 1. Redirect the user to the authorization URL
const authUrl = await oauth2.createAuthorizationURL();
console.log(authUrl);
// 2. Handle the callback — the SDK exchanges the code for tokens
await oauth2.handleAuthorizationCallback(new URL(callbackUrl));
// 3. Get an authenticated client
const client = oauth2.getAuthenticatedClient();Device code flow (for CLIs and terminal apps):
const oauth2 = createOAuth2Client({
clientId: 'your-client-id',
issuer: new URL('https://exist.io/'),
tokenStore: new MemoryTokenStore(),
flow: 'device_code',
});
const deviceAuth = await oauth2.startDeviceAuthorization();
console.log(`Open ${deviceAuth.verification_uri_complete} and enter code ${deviceAuth.user_code}`);
await oauth2.pollForTokens(deviceAuth);
const client = oauth2.getAuthenticatedClient();import {getProfile} from '@fromo/exist-sdk';
import type {UserProfile} from '@fromo/exist-sdk';
const profile: UserProfile = await getProfile(client);
console.log(profile.first_name, profile.last_name);import {getAttributesWithValues} from '@fromo/exist-sdk';
import type {PagedAttributesWithValues} from '@fromo/exist-sdk';
const attributes: PagedAttributesWithValues = await getAttributesWithValues(client, {
days: 7,
limit: 50,
});
for (const attr of attributes.results ?? []) {
console.log(`${attr.label}: ${attr.values?.length} values`);
}import {getAverages} from '@fromo/exist-sdk';
const averages = await getAverages(client, {limit: 20});
for (const row of averages.results ?? []) {
console.log(row.attribute, row.value);
}import {getCorrelations, getCorrelationCombo} from '@fromo/exist-sdk';
// All correlations for the user
const correlations = await getCorrelations(client, {confident: 1});
// Specific attribute pair
const combo = await getCorrelationCombo(client, {
attribute: 'sleep_duration',
attribute2: 'mood',
});import {acquireAttributes, updateAttributeValues, incrementAttributeValues} from '@fromo/exist-sdk';
// Start tracking new attributes
await acquireAttributes(client, [{name: 'coffee_cups'}]);
// Update today's value
await updateAttributeValues(client, [{name: 'coffee_cups', date: '2026-04-15', value: 2}]);
// Increment an attribute
await incrementAttributeValues(client, [{name: 'coffee_cups', value: 1}]);All errors are typed as ExistError:
import {getProfile} from '@fromo/exist-sdk';
import type {ExistError} from '@fromo/exist-sdk';
try {
await getProfile(client);
} catch (err) {
const e = err as ExistError;
console.error(e.status, e.message, e.code);
}Runtime Zod validation errors include cause with the specific schema failures:
try {
await getProfile(client);
} catch (err) {
const e = err as ExistError;
if (e.cause) {
console.error('API shape mismatch:', e.cause);
}
}import {createClient} from '@fromo/exist-sdk';
const client = createClient({
token: 'your-token',
fetch: customFetchImpl, // e.g., for testing with MSW
});// Different tokens, same base URL
const clientA = createClient({token: tokenA});
const clientB = createClient({token: tokenB});import {createOAuth2Client, type TokenStore} from '@fromo/exist-sdk';
const customStore: TokenStore = {
getAccessToken: () => process.env.EXIST_ACCESS_TOKEN,
getRefreshToken: () => process.env.EXIST_REFRESH_TOKEN,
setTokens: (tokens) => {
/* persist somewhere */
},
};
const oauth2 = createOAuth2Client({
clientId: 'your-client-id',
issuer: new URL('https://exist.io/'),
tokenStore: customStore,
flow: 'device_code',
});- Node.js 18+ or any runtime with native
fetch - An exist.io account
Full API documentation: developer.exist.io
TypeScript types are generated from the OpenAPI spec in src/types.ts. Regenerate with:
yarn generate-typesSee CONTRIBUTING.md. PRs welcome.