A typed Dart client for the Splitwise API v3.0,
generated from and tested against Splitwise's official OpenAPI specification.
Works in Dart and Flutter apps on every platform (no dart:io).
- OAuth 2.0 authorization-code flow and personal API keys
- Every documented endpoint (users, groups, friends, expenses, comments, notifications, currencies, categories) — verified by a test that fails if the spec and the client drift apart
- Typed request and response models with
fromJson/toJson - Failures surface as exceptions, including Splitwise's
"200 OK but
success: false" responses - Injectable
http.Clientfor testing - Unknown enum values decode to
unknowninstead of throwing
Upgrading from 2.x? See the migration guide — 3.0.0 replaces OAuth 1.0 and the raw-string responses.
dependencies:
splitwise_api: ^3.0.0Generate a personal API key on your app's page at https://secure.splitwise.com/apps. The key acts on your own account.
import 'package:splitwise_api/splitwise_api.dart';
Future<void> main() async {
final client = SplitwiseClient.apiKey('YOUR_API_KEY');
final me = await client.getCurrentUser();
print('Hi ${me.firstName}!');
final groups = await client.getGroups();
final expenses = await client.getExpenses(groupId: groups.first.id, limit: 10);
for (final expense in expenses) {
print('${expense.description}: ${expense.cost} ${expense.currencyCode}');
}
client.close();
}Use OAuth 2.0 to act on behalf of other users. Register an application at https://secure.splitwise.com/apps to get a consumer key/secret and set its callback URL.
final oauth = SplitwiseOAuth2(
clientId: 'CONSUMER_KEY',
clientSecret: 'CONSUMER_SECRET',
redirectUri: Uri.parse('https://example.com/splitwise/callback'),
);
// 1. Send the user to the authorization page. Keep `state` (per session,
// e.g. in the user's server-side session) to validate the callback.
final state = SplitwiseOAuth2.generateState();
final url = oauth.authorizationUrl(state: state);
// 2. Splitwise redirects to redirectUri?code=...&state=...
// Reject the callback unless its state matches — this binds the response
// to the request you started and blocks login CSRF.
if (callback.queryParameters['state'] != state) {
throw StateError('OAuth state mismatch');
}
final token = await oauth.exchangeCode(callback.queryParameters['code']!);
// 3. Persist the token and use it.
await storage.write('splitwise_token', jsonEncode(token.toJson()));
final client = SplitwiseClient.accessToken(token.accessToken);Restore a persisted token with OAuth2Token.fromJson(jsonDecode(saved)).
Splitwise does not document token expiry or refresh tokens; OAuth2Token
keeps expiresAt/refreshToken nullable in case that changes.
| Area | Methods |
|---|---|
| Users | getCurrentUser(), getUser(id), updateUser(id, UpdateUserRequest) |
| Groups | getGroups(), getGroup(id), createGroup(CreateGroupRequest), deleteGroup(id), undeleteGroup(id), addUserToGroup(groupId:, userId:), addUserToGroupByEmail(groupId:, email:, firstName:, lastName:), removeUserFromGroup(groupId:, userId:) |
| Friends | getFriends(), getFriend(id), createFriend(email:, …), createFriends([NewFriend…]), deleteFriend(id) |
| Expenses | getExpense(id), getExpenses(groupId:, friendId:, datedAfter:, datedBefore:, updatedAfter:, updatedBefore:, limit:, offset:), `createExpense(EqualGroupSplit |
| Comments | getComments(expenseId:), createComment(expenseId:, content:), deleteComment(id) |
| Other | getNotifications(updatedAfter:, limit:), getCurrencies(), getCategories() |
// Split equally between everyone in a group.
await client.createExpense(EqualGroupSplit(
cost: '30.00',
description: 'Groceries',
groupId: group.id,
));
// Specify each person's share (amounts are decimal strings).
await client.createExpense(SplitByShares(
cost: '30.00',
description: 'Taxi',
groupId: 0, // 0 = not in a group
users: [
ExpenseShareInput.user(userId: me.id, paidShare: '30.00', owedShare: '15.00'),
ExpenseShareInput.user(userId: friend.id, paidShare: '0', owedShare: '15.00'),
],
));
// Change only what you pass.
await client.updateExpense(expense.id, UpdateExpenseRequest(description: 'Cab'));try {
await client.deleteExpense(id);
} on SplitwiseUnauthorizedException {
// 401 — bad or revoked API key / token
} on SplitwiseNotFoundException catch (e) {
print(e.errors); // e.g. "Invalid API request: record not found"
} on SplitwiseRequestFailedException catch (e) {
// Splitwise answered 200 but reported failure, e.g. already deleted
print(e.errors.byField); // {expense: [Expense has already been deleted]}
} on SplitwiseHttpException catch (e) {
print('HTTP ${e.statusCode}');
}All exceptions extend SplitwiseException. SplitwiseRateLimitException
(429) exposes retryAfter when the server sends one; the client does not
retry automatically.
Splitwise documents JSON request bodies and this package sends them by default. If an endpoint rejects a JSON body you can switch to form encoding (the encoding the Splitwise web app uses):
final client = SplitwiseClient.apiKey(key, bodyEncoding: BodyEncoding.formUrlEncoded);createFriends is always form-encoded because the server does not accept
JSON for it.
Pass a MockClient from package:http/testing.dart:
final client = SplitwiseClient.apiKey('k', httpClient: MockClient((request) async {
return http.Response(jsonEncode({'user': {'id': 1}}), 200);
}));dart pub get
dart run build_runner build # regenerate *.g.dart after editing models
dart format .
dart analyze
dart test # offline suite against the committed spec
SPLITWISE_API_KEY=... dart test --tags live # optional: hits the real APIThe live suite creates and deletes a throwaway group, expense and comment on the key's own account and never touches other users.
MIT — see LICENSE.