Skip to content

keriwarr/splitwise

Repository files navigation

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

About

A JavaScript SDK for the Splitwise API.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors