npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

splitwise

v2.0.0

Published

A TypeScript SDK for the Splitwise API.

Readme

Splitwise SDK

A TypeScript SDK for the Splitwise API. Zero runtime dependencies, full TypeScript types, supports both Client Credentials and Authorization Code (PKCE) OAuth flows.

Heads up: v2 was largely written by Claude under the direction of the maintainer (@keriwarr). The result has been reviewed by humans and validated against the live API, but you should treat the SDK with the same scrutiny you'd apply to any new dependency. Bug reports welcome.

Install

npm install splitwise
# or
yarn add splitwise

Requires Node.js 18 or newer (the SDK uses the global fetch and Web Crypto APIs).

Quick start

import { Splitwise } from 'splitwise';

const sw = new Splitwise({
  consumerKey: process.env.SPLITWISE_CONSUMER_KEY,
  consumerSecret: process.env.SPLITWISE_CONSUMER_SECRET,
});

const me = await sw.users.getCurrent();
console.log(`Hi, ${me.firstName}!`);

What's new in v2

v2 is a ground-up rewrite. If you're upgrading from v1, see Migrating from v1 below. The highlights:

  • Resource-namespaced API (sw.expenses.list(), sw.users.getCurrent(), ...).
  • Full TypeScript types for every request and response.
  • camelCase throughout — no more group_id or paid_share.
  • Promise-only API (no callbacks).
  • Authorization Code + PKCE flow for end-user access, in addition to Client Credentials.
  • Typed error hierarchy with automatic retry for transient failures.
  • Async-iterable pagination on list endpoints.
  • Zero runtime dependencies.

Authentication

The SDK supports three ways to authenticate.

1. Pre-obtained access token

The simplest option — useful when you've already obtained a token (e.g. you cached one from a previous run, or you obtained it through a separate flow).

const sw = new Splitwise({ accessToken: 'sw_abc123...' });

2. Client Credentials

Use this to access the app owner's data (i.e. the developer who registered the app). The SDK fetches an access token automatically on first use and caches it in memory.

const sw = new Splitwise({
  consumerKey: process.env.SPLITWISE_CONSUMER_KEY,
  consumerSecret: process.env.SPLITWISE_CONSUMER_SECRET,
});

If you'd like to obtain the token explicitly (e.g. to persist it across process restarts), call sw.getAccessToken():

const token = await sw.getAccessToken();
// store `token` somewhere safe, then later:
const sw2 = new Splitwise({ accessToken: token });

3. Authorization Code with PKCE

Use this to access end-user data — your application redirects the user through Splitwise's consent screen, then exchanges the returned code for an access token tied to that user. PKCE is enforced.

The SDK does not run a redirect server — your application is responsible for handling the OAuth callback URL.

import { Splitwise } from 'splitwise';

// Step 1: build the authorization URL and persist `state` + `codeVerifier`
// (e.g. in the user's session). Then redirect the user to `url`.
const auth = await Splitwise.createAuthorizationUrl({
  clientId: process.env.SPLITWISE_CLIENT_ID!,
  redirectUri: 'https://example.com/oauth/callback',
});

// session.state = auth.state;
// session.codeVerifier = auth.codeVerifier;
// res.redirect(auth.url);

// Step 2: in your callback handler, verify `state` matches what you stored,
// then exchange the code for a fully configured Splitwise client.
const sw = await Splitwise.fromAuthorizationCode({
  clientId: process.env.SPLITWISE_CLIENT_ID!,
  clientSecret: process.env.SPLITWISE_CLIENT_SECRET!,
  code: req.query.code as string,
  codeVerifier: session.codeVerifier,
  redirectUri: 'https://example.com/oauth/callback',
});

const me = await sw.users.getCurrent();

Splitwise.fromAuthorizationCode accepts an optional second argument with any non-auth SplitwiseConfig options (e.g. timeout, logger).

API reference

For the underlying API surface and detailed parameter semantics, see the official Splitwise API docs.

Expenses — sw.expenses

| Method | Description | |---|---| | list(params?: ExpenseListParams): PagedResult<Expense> | List expenses (paginated). Filterable by groupId, friendId, date range, etc. | | get(params: { id }): Promise<Expense> | Fetch a single expense. | | create(params: ExpenseCreateParams): Promise<Expense> | Create an expense. | | update(params: ExpenseUpdateParams): Promise<Expense> | Update an existing expense by id. | | delete(params: { id }): Promise<void> | Delete an expense (soft delete). Throws SplitwiseConstraintError on domain failure. | | restore(params: { id }): Promise<void> | Restore a soft-deleted expense. | | createDebt(params: CreateDebtParams): Promise<Expense> | Convenience helper for the common "user A owes user B X" case. |

// Create a $25 dinner expense, split equally between three users
const expense = await sw.expenses.create({
  cost: '25.00',
  description: 'Dinner',
  groupId: 12345,
  splitEqually: true,
});

// Convenience: record that user 42 owes user 23 ten dollars (user 23 paid for it)
await sw.expenses.createDebt({
  paidBy: 23,
  owedBy: 42,
  amount: 10,
  description: 'Coffee',
  groupId: 12345,
});

Groups — sw.groups

| Method | Description | |---|---| | list(): Promise<Group[]> | List the current user's groups. | | get(params: { id }): Promise<Group> | Fetch a single group. | | create(params: GroupCreateParams): Promise<Group> | Create a new group. | | delete(params: { id }): Promise<void> | Delete a group. Throws SplitwiseConstraintError on domain failure. | | restore(params: { id }): Promise<void> | Restore a deleted group. | | addUser(params: AddUserToGroupParams): Promise<User> | Add a user to a group (by userId or by name + email). Returns the added user. | | removeUser(params: { groupId, userId }): Promise<void> | Remove a user from a group. |

Users — sw.users

| Method | Description | |---|---| | getCurrent(): Promise<CurrentUser> | Fetch the authenticated user. | | get(params: { id }): Promise<User> | Fetch any user by id. | | update(params: UserUpdateParams): Promise<User> | Update a user's profile fields. |

Friends — sw.friends

| Method | Description | |---|---| | list(): Promise<Friend[]> | List the current user's friends. | | get(params: { id }): Promise<Friend> | Fetch one friend. | | create(params: FriendCreateParams): Promise<Friend> | Add a friend by email. | | createMultiple(params: FriendCreateMultipleParams): Promise<Friend[]> | Add several friends at once. | | delete(params: { id }): Promise<void> | Remove a friend. Throws SplitwiseConstraintError if the friendship has unsettled debts. |

Comments — sw.comments

| Method | Description | |---|---| | list(params: { expenseId }): Promise<Comment[]> | List comments on an expense. | | create(params: { expenseId, content }): Promise<Comment> | Post a comment on an expense. | | delete(params: { id }): Promise<Comment> | Delete a comment (returns the deleted comment). |

Notifications — sw.notifications

| Method | Description | |---|---| | list(params?: NotificationListParams): Promise<Notification[]> | List notifications, optionally filtered by updatedAfter. |

Currencies — sw.currencies

| Method | Description | |---|---| | list(): Promise<Currency[]> | List all currencies supported by Splitwise. |

Categories — sw.categories

| Method | Description | |---|---| | list(): Promise<Category[]> | List all expense categories (with their subcategories). |

Top-level utilities

| Method | Description | |---|---| | sw.test(): Promise<{ clientId, token, requestUrl, params }> | "Whoami" endpoint: returns the authenticated client's id and token info. | | sw.parseSentence(params: ParseSentenceParams): Promise<ParseSentenceResponse> | Parse a natural-language description (e.g. "I owe Bob $10") into an expense. | | sw.getMainData(params?: GetMainDataParams): Promise<unknown> | Bulk fetch user, groups, friends, currencies, and categories in one call. | | sw.getAccessToken(): Promise<string> | Return the current access token (fetching one via Client Credentials if needed). |

Pagination

sw.expenses.list() returns a PagedResult<Expense> that can be used three ways. Other resources currently return plain arrays.

Await it for the first page — sends your limit as-is, so the server's default applies if you omit it. Repeated awaits return the cached result.

const firstPage = await sw.expenses.list({ groupId: 12345, limit: 50 });

Async-iterate to walk every item — the SDK pages behind the scenes (default page size 100; honors your limit if set).

for await (const expense of sw.expenses.list({ groupId: 12345 })) {
  console.log(expense.description, expense.cost);
}

Iterate page-by-page with byPage():

for await (const page of sw.expenses.list({ groupId: 12345 }).byPage()) {
  console.log(`Got ${page.length} expenses`);
}

Error handling

Every error thrown by the SDK extends SplitwiseError. HTTP errors are mapped to specific subclasses so you can branch on instanceof:

SplitwiseError
├── SplitwiseApiError              (any failure response from the API)
│   ├── SplitwiseValidationError       (400)
│   ├── SplitwiseAuthenticationError   (401)
│   ├── SplitwiseForbiddenError        (403)
│   ├── SplitwiseNotFoundError         (404)
│   ├── SplitwiseRateLimitError        (429)
│   ├── SplitwiseServerError           (5xx)
│   └── SplitwiseConstraintError       (200 with success:false / non-empty errors)
└── SplitwiseConnectionError       (network failures)

SplitwiseConstraintError covers cases where the API returns HTTP 200 but the operation didn't actually happen — typically because of a domain rule (e.g. trying to delete a friend with whom you have an unsettled balance). Following the Stripe pattern, the SDK throws on these rather than returning the failure as data, so delete()-style methods can have a clean Promise<void> return type and callers don't need to remember to check a success field.

import {
  Splitwise,
  SplitwiseAuthenticationError,
  SplitwiseConstraintError,
  SplitwiseRateLimitError,
  SplitwiseNotFoundError,
} from 'splitwise';

try {
  const expense = await sw.expenses.get({ id: 999 });
} catch (err) {
  if (err instanceof SplitwiseNotFoundError) {
    // ...the expense doesn't exist or isn't visible to this user
  } else if (err instanceof SplitwiseAuthenticationError) {
    // ...token is invalid or expired
  } else if (err instanceof SplitwiseRateLimitError) {
    // `retryAfter` is the parsed Retry-After value in seconds (handles both
    // delta-seconds and HTTP-date formats); undefined if the server didn't
    // send the header.
    await sleep((err.retryAfter ?? 1) * 1000);
  } else {
    throw err;
  }
}

// Domain failure example: trying to delete a friend with unsettled debts
try {
  await sw.friends.delete({ id: 12345 });
} catch (err) {
  if (err instanceof SplitwiseConstraintError) {
    // err.message: "You cannot delete this friendship; you have unsettled debts"
    // err.raw: full response body for inspection
    showUserMessage(err.message);
  }
}

SplitwiseApiError carries the HTTP statusCode, an SDK-assigned error code, and the unparsed raw response body for inspection. SplitwiseConnectionError exposes the underlying network error as cause.

The SDK automatically retries transient failures (network errors, 429s, and 5xx responses) up to maxRetries times with exponential backoff before throwing.

Configuration options

new Splitwise(config) accepts the following options:

| Option | Type | Default | Description | |---|---|---|---| | consumerKey | string | — | OAuth consumer key. Required when accessToken is not provided. | | consumerSecret | string | — | OAuth consumer secret. Required when accessToken is not provided. | | accessToken | string | — | Pre-obtained access token. If set, the SDK skips the OAuth flow entirely. | | baseUrl | string | https://secure.splitwise.com/api/v3.0 | Override the API base URL (useful for testing). | | maxRetries | number | 2 | Maximum automatic retries for transient failures. | | timeout | number | 30000 | Per-request timeout in milliseconds. | | logger | Logger | — | Custom logger; the SDK never calls console.* directly. Implements { debug, info, warn, error }. | | logLevel | 'none' \| 'error' \| 'warn' \| 'info' \| 'debug' | 'none' | Filter logs at or below this level. | | fetch | typeof fetch | global fetch | Inject a custom fetch implementation (useful for testing). | | hooks | Hooks | — | Lifecycle callbacks (onRequest, onResponse, onError) for observability. Hooks are called per HTTP attempt; the Authorization header is redacted in event payloads. | | appInfo | AppInfo | — | Identifies the calling app in the User-Agent header ({ name, version?, url? }). Helpful for support and your own telemetry. |

You must provide either accessToken or both consumerKey and consumerSecret.

Migrating from v1

v2 is a breaking API change end-to-end. The mapping is mostly mechanical, but read carefully:

  • Resource namespaces. All methods have moved under resource objects.

    | v1 | v2 | |---|---| | sw.getCurrentUser() | sw.users.getCurrent() | | sw.getUser({ id }) | sw.users.get({ id }) | | sw.getGroups() | sw.groups.list() | | sw.getGroup({ id }) | sw.groups.get({ id }) | | sw.createGroup(...) | sw.groups.create(...) | | sw.deleteGroup({ id }) | sw.groups.delete({ id }) | | sw.addUserToGroup(...) | sw.groups.addUser(...) | | sw.removeUserFromGroup(...) | sw.groups.removeUser(...) | | sw.getExpenses(...) | sw.expenses.list(...) | | sw.getExpense({ id }) | sw.expenses.get({ id }) | | sw.createExpense(...) | sw.expenses.create(...) | | sw.updateExpense(...) | sw.expenses.update(...) | | sw.deleteExpense({ id }) | sw.expenses.delete({ id }) | | sw.getFriends() | sw.friends.list() | | sw.getNotifications() | sw.notifications.list() | | sw.getCurrencies() | sw.currencies.list() | | sw.getCategories() | sw.categories.list() | | sw.createDebt(...) | sw.expenses.createDebt(...) |

  • camelCase everywhere. All inputs and outputs use camelCase. group_id becomes groupId, paid_share becomes paidShare, default_currency becomes defaultCurrency, and so on. The SDK converts to and from the API's snake_case at the HTTP boundary.

  • Promise-only. The optional callback argument is gone. All methods return Promises.

  • Default IDs removed. v1 let you pass group_id, user_id, expense_id, or friend_id to the constructor and have them applied implicitly. v2 requires explicit IDs on every method call. Passing any of these to the constructor now throws TypeError with a migration hint.

  • sw.test() is a whoami. It now returns { clientId, token, requestUrl, params } rather than a generic boolean. Use it to confirm which app/token the SDK is using.

  • Delete and membership endpoints throw on failure. sw.expenses.delete, sw.expenses.restore, sw.groups.delete, sw.groups.restore, and sw.groups.removeUser and sw.friends.delete now return Promise<void>; sw.groups.addUser returns the added User. A SplitwiseConstraintError is thrown if the API rejects the operation (e.g. unsettled debts when deleting a friend).

  • expenses.list({ friendshipId }) is now expenses.list({ friendId }). v1's friendship_id was a typo — the actual API parameter is friend_id, so filter-by-friend has been silently broken since v1.

  • TypeScript types throughout. Every request and response is fully typed. You no longer need a separate @types/splitwise package.

  • Zero runtime dependencies. v2 uses the platform fetch and Web Crypto APIs and has no dependencies in package.json.

  • New: end-user OAuth. v2 ships an Authorization Code + PKCE flow for accessing data belonging to users other than the app owner — a long-standing limitation of v1.

Examples

Two runnable demos live in examples/:

  • cli-client-credentials/ — a small CLI that uses Client Credentials to print a summary of the authenticated user's account.
  • web-auth-code/ — a tiny Express app that logs users in via the Authorization Code + PKCE flow and shows their dashboard.

Each example has its own README and is set up as a self-contained project. Use them as a copy-paste starting point for your own integrations.

License

MIT