Skip to main content

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:

  1. Immutable fields: description and currency on tricounts can only be set at creation time
  2. Member deletion: Members with transactions cannot be fully deleted; they become DELETED status but remain in data
  3. 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tricount_api-0.1.2.tar.gz (19.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tricount_api-0.1.2-py3-none-any.whl (20.5 kB view details)

Uploaded Python 3

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

Hashes for tricount_api-0.1.2.tar.gz
Algorithm Hash digest
SHA256 eaf9056d99546004ba8514074f0bd2baa871e67185590d6f47967a683f9ced99
MD5 e9b1e5fc599b9bbd7908c46c03d5f971
BLAKE2b-256 c9f54a220cc86b0992ec42b1b4508ff6242ef1a99c013c4ac193be22bf977ba0

See more details on using hashes here.

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

Hashes for tricount_api-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 6300fa8e7cbd7d2f8205c7bcd130464dd3399d4aa153576c201d11518b195518
MD5 04269357b3ac9548401506afbb2fbeca
BLAKE2b-256 d5aad727cc8c73136c16127bc26f2367bbf8a98d6bae7923203025b99f3d56ca

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page