Unofficial Python client for the Tricount (bunq) API
Project description
Tricount API Client
An unofficial Python client for the Tricount (bunq) API, reverse-engineered from the Android app.
Features
- Full read/write access to Tricount data
- Create, edit, and delete transactions
- Manage members and splits
- Support for custom categories, foreign currencies, and attachments
- No account linking required - just needs the sharing link to modify any tricount
Installation
pip install tricount-api
Quick Start
from tricount import load_client
# Create an authenticated client (auto-generates credentials on first use)
client = load_client()
# Join any tricount using its sharing link - no need to be a member!
tricount = client.join_tricount("tABC123xyz")
print(f"Tricount: {tricount.title}")
print(f"Members: {[m.display_name for m in tricount.members]}")
# Create a transaction (as any member)
client.create_transaction(
tricount=tricount,
description="Dinner",
amount=5000,
payer=tricount.members[0],
split_among=tricount.members
)
Authentication
The API uses device-based authentication. On first use, credentials are automatically generated and saved to tricount_credentials.json. These credentials are reused for subsequent sessions.
from pathlib import Path
from tricount import Credentials, TricountAPI
# Manual credential management
creds = Credentials.generate()
creds.save(Path("my_credentials.json"))
# Or load existing credentials
creds = Credentials.load(Path("my_credentials.json"))
# Create and authenticate client
client = TricountAPI(creds)
user_id = client.authenticate()
Core Concepts
Tricount Structure
- Tricount: An expense group with members and transactions
- Member: A participant in the tricount (identified by UUID)
- Transaction: An expense, income, or reimbursement
- Allocation: How a transaction is split among members
Amount Convention
- Expenses: Stored as negative string values (e.g.,
"-1000"for a ¥1000 expense) - Income: Stored as positive string values
- Values are in full currency units, not cents (e.g.,
"1500"= 1500 JPY or 15.00 EUR) - The client handles this automatically - you always pass positive amounts when creating transactions
The Amount class provides helper properties for working with amounts:
tx = tricount.transactions[0]
print(tx.amount.value) # Raw string value, e.g., "-1500"
print(tx.amount.as_float) # As float with sign, e.g., -1500.0
print(tx.amount.as_abs) # Absolute value as float, e.g., 1500.0
print(tx.amount.currency) # Currency code, e.g., "JPY"
Transaction Types
| Type | Description | Amount Sign |
|---|---|---|
NORMAL |
Regular expense (someone paid for something) | Negative |
INCOME |
Money received by the group (refunds, etc.) | Positive |
BALANCE |
Reimbursement between members | Positive |
Usage Examples
Joining a Tricount
The API allows you to join and modify any tricount using just its sharing link. You don't need to be an existing member - anyone with the link can make changes.
# Join a tricount by its sharing token (from the URL: tricount.com/tXXXXX)
# By default, fetches full data including all transactions
tricount = client.join_tricount("tABC123xyz")
# For faster joins when you don't need transaction history:
tricount = client.join_tricount("tABC123xyz", fetch_full=False)
# Now you can create/edit/delete transactions as any member
# The bot doesn't need to be added as a member
client.create_transaction(
tricount=tricount,
description="Coffee",
amount=500,
payer=tricount.members[0], # Pay as any existing member
split_among=tricount.members
)
Managing Tricounts
# Create a new tricount
tricount_id = client.create_tricount(
title="Trip to Tokyo",
currency="JPY",
description="Summer vacation expenses"
)
# List all tricounts synced to your account
tricounts = client.list_tricounts()
# Read-only access (without joining)
tricount = client.get_tricount("tXXXXX")
# Update tricount metadata
client.update_tricount(tricount, title="Tokyo 2024", emoji="🗼")
# Archive/unarchive
client.archive_tricount(tricount)
client.unarchive_tricount(tricount)
# Leave a tricount (remove from your synced list, doesn't delete it)
client.leave_tricount(tricount)
# Delete permanently (only for tricounts you created)
client.delete_tricount(tricount)
Managing Members
# Add members
client.add_members(tricount, ["Alice", "Bob", "Charlie"])
# Rename a member
alice = tricount.get_member_by_name("Alice")
client.rename_member(tricount, alice, "Alice Smith")
# Delete a member (only works if they have no transactions)
client.delete_member(tricount, alice)
# When you join a tricount, you're auto-linked to the first member
tricount = client.join_tricount("tXXXXX")
print(f"Auto-linked to: {tricount.linked_member.display_name}")
# Switch to a different member
client.link_to_member(tricount, bob)
# Check who you're linked to
tricount = client.get_tricount_by_id(tricount.id) # Refresh to get membership
if tricount.linked_member:
print(f"I am: {tricount.linked_member.display_name}")
# Note: You can create transactions as any member, regardless of who you're linked to
# The link is just used by the Tricount app to show "your" balance
# Once linked, you can only switch members, not unlink completely
Creating Expenses
from tricount import Category
# Simple expense split equally
alice = tricount.get_member_by_name("Alice")
bob = tricount.get_member_by_name("Bob")
tx_id = client.create_transaction(
tricount=tricount,
description="Dinner at restaurant",
amount=5000, # Always positive
payer=alice,
split_among=[alice, bob],
category=Category.FOOD_AND_DRINK,
)
# Custom split (unequal amounts)
tx_id = client.create_transaction_custom_split(
tricount=tricount,
description="Hotel room",
amount=10000,
payer=alice,
allocations=[
(alice, 3000), # Alice pays 3000
(bob, 7000), # Bob pays 7000
],
)
# Ratio-based split
tx_id = client.create_transaction_ratio_split(
tricount=tricount,
description="Group activity",
amount=9000,
payer=alice,
split_ratios=[
(alice, 1), # Alice: 1/4 = 2250
(bob, 2), # Bob: 2/4 = 4500
(charlie, 1), # Charlie: 1/4 = 2250
],
)
Income Transactions
# Record income (e.g., refund, lottery, sold items)
tx_id = client.create_income(
tricount=tricount,
description="Tax refund",
amount=3000,
receiver=alice, # Who received the money
split_among=[alice, bob], # Credit split among
)
Reimbursements
# Record a payment between members
tx_id = client.create_reimbursement(
tricount=tricount,
payer=bob, # Bob pays back...
receiver=alice, # ...to Alice
amount=2500,
description="Settling up",
)
Foreign Currency
# Auto-fetch exchange rate
tx_id = client.create_transaction(
tricount=tricount, # JPY tricount
description="Coffee in NYC",
amount=15, # 15 USD
payer=alice,
split_among=[alice, bob],
currency="USD", # Original currency
)
# Manual exchange rate
tx_id = client.create_transaction(
tricount=tricount,
description="Souvenir",
amount=100, # 100 USD
payer=alice,
split_among=[alice, bob],
currency="USD",
exchange_rate=150, # 1 USD = 150 JPY
)
# Get exchange rates
rates = client.get_exchange_rates("USD")
print(rates["JPY"]) # e.g., 149.5
Editing & Deleting Transactions
# Edit a transaction
client.edit_transaction(
tricount=tricount,
transaction_id=123,
description="Updated description",
amount=6000,
category=Category.SHOPPING,
)
# Delete a transaction
client.delete_transaction(tricount, transaction_id=123)
Attachments
Gallery Attachments (standalone images)
from pathlib import Path
# Upload to gallery
attachment_uuid = client.upload_gallery_attachment(
tricount,
Path("receipt.jpg"),
)
# List gallery
attachments = client.list_gallery_attachments(tricount)
for att in attachments:
print(f"{att.uuid}: {att.original_url}")
# Delete from gallery
client.delete_gallery_attachment(tricount, attachment_uuid)
Transaction Attachments (receipts linked to expenses)
# Upload attachment
attachment_id = client.upload_transaction_attachment(
tricount,
Path("receipt.jpg"),
)
# Create transaction with attachment
tx_id = client.create_transaction(
tricount=tricount,
description="Groceries",
amount=3500,
payer=alice,
split_among=[alice, bob],
attachment_ids=[attachment_id],
)
# Add attachment to existing transaction
client.add_transaction_attachment(tricount, tx_id, attachment_id)
# Remove attachment from transaction
client.remove_transaction_attachment(tricount, tx_id, attachment_id)
Calculating Balances
# Get current balances
balances = client.get_balances(tricount)
for name, balance in balances.items():
if balance > 0:
print(f"{name} is owed {balance:.0f} {tricount.currency}")
elif balance < 0:
print(f"{name} owes {-balance:.0f} {tricount.currency}")
else:
print(f"{name} is settled up")
Syncing Multiple Tricounts
# Efficiently fetch multiple tricounts at once
result = client.sync_tricounts(
active_tokens=["token1", "token2"],
archived_tokens=["token3"],
)
for tc in result["active"]:
print(f"Active: {tc.title}")
for tc in result["archived"]:
print(f"Archived: {tc.title}")
Categories
Standard Categories
Available built-in expense categories (use with Category enum):
| Category | Emoji | Description |
|---|---|---|
TRAVEL |
🛏 | Accommodation |
ENTERTAINMENT |
🎤 | Entertainment |
GROCERIES |
🛒 | Groceries |
HEALTHCARE |
🦷 | Healthcare |
INSURANCE |
🧯 | Insurance |
RENT_AND_UTILITIES |
🏠 | Rent & Utilities |
FOOD_AND_DRINK |
🍔 | Restaurants |
SHOPPING |
🛍 | Shopping |
TRANSPORT |
🚕 | Transport |
OTHER |
✋ | Other |
# Use a standard category
client.create_transaction(
tricount=tricount,
description="Taxi ride",
amount=25.00,
payer=alice,
split_among=[alice, bob],
category=Category.TRANSPORT,
)
Custom Categories
You can create custom categories with a label and emoji. These are stored per-transaction using the category_custom parameter:
# Use a custom category with label and emoji
client.create_transaction(
tricount=tricount,
description="Morning latte",
amount=5.50,
payer=alice,
split_among=[alice, bob],
category_custom="Coffee ☕️", # Format: "Label Emoji"
)
# Another example
client.create_transaction(
tricount=tricount,
description="Board game night supplies",
amount=30.00,
payer=bob,
split_among=[alice, bob],
category_custom="Game Night 🎲",
)
When category_custom is provided, the category is automatically set to OTHER and the custom label+emoji is displayed in the app.
You can list all custom categories used in a tricount:
# Get all unique custom categories from transactions
custom_cats = client.get_custom_categories(tricount)
for cat in custom_cats:
print(cat) # e.g., "Coffee ☕️", "Game Night 🎲"
Data Classes
Tricount
@dataclass
class Tricount:
id: int
uuid: str
title: str
description: str
currency: str
public_identifier_token: str # Sharing token
members: list[Member]
transactions: list[Transaction]
emoji: Optional[str]
category: Optional[str]
status: str # "READ_WRITE" or "READ_ONLY"
Member
@dataclass
class Member:
id: int
uuid: str
display_name: str
status: str # "ACTIVE", "INACTIVE", "DELETED"
@property
def membership_uuid(self) -> str:
"""Alias for uuid, for consistency with Transaction/Allocation"""
Note: Member.membership_uuid is an alias for Member.uuid, provided for consistency
with Transaction.membership_uuid_owner and Allocation.membership_uuid.
Transaction
@dataclass
class Transaction:
id: Optional[int]
uuid: str
description: str
amount: Amount
membership_uuid_owner: str # Who paid
allocations: list[Allocation]
date: str
status: TransactionStatus
transaction_type: TransactionType # NORMAL, INCOME, BALANCE
category: Optional[str]
category_custom: Optional[str]
API Limitations
Based on reverse engineering, some limitations were discovered:
- Immutable fields:
descriptionandcurrencyon tricounts can only be set at creation time - Member deletion: Members with transactions cannot be fully deleted; they become
DELETEDstatus but remain in data - Settlement endpoints: May require a bunq banking account (returns 404 for regular users)
Error Handling
The client raises requests.HTTPError for API errors:
try:
tricount = client.get_tricount("invalid_token")
except requests.HTTPError as e:
if e.response.status_code == 404:
print("Tricount not found")
else:
print(f"API error: {e}")
License
This is an unofficial client created through reverse engineering for educational purposes. Use responsibly and in accordance with Tricount's terms of service.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tricount_api-0.1.2.tar.gz.
File metadata
- Download URL: tricount_api-0.1.2.tar.gz
- Upload date:
- Size: 19.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.21 {"installer":{"name":"uv","version":"0.9.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eaf9056d99546004ba8514074f0bd2baa871e67185590d6f47967a683f9ced99
|
|
| MD5 |
e9b1e5fc599b9bbd7908c46c03d5f971
|
|
| BLAKE2b-256 |
c9f54a220cc86b0992ec42b1b4508ff6242ef1a99c013c4ac193be22bf977ba0
|
File details
Details for the file tricount_api-0.1.2-py3-none-any.whl.
File metadata
- Download URL: tricount_api-0.1.2-py3-none-any.whl
- Upload date:
- Size: 20.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.21 {"installer":{"name":"uv","version":"0.9.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6300fa8e7cbd7d2f8205c7bcd130464dd3399d4aa153576c201d11518b195518
|
|
| MD5 |
04269357b3ac9548401506afbb2fbeca
|
|
| BLAKE2b-256 |
d5aad727cc8c73136c16127bc26f2367bbf8a98d6bae7923203025b99f3d56ca
|