Staging
Not connected
Total
all expenses
Pending
awaiting action
Approved
ready to book
Rejected
need review
From To
Date Employee Category Project Amount Currency Exch. Amount Exch. CCY Journal ID VAT 📎 Status Actions
Paid expenses
From To
DateEmployeeCategory ProjectAmount CCYExch. Amount Exch. CCYJournal ID📎 StatusActions
1 — Ready for payroll
2 — Journal IDs
3 — Paid
4 — Booked
Generate journal ID
All approved expenses will be assigned the next journal ID and moved to Ready.
Next journal ID
Approved expenses
Ready expenses
DateEmployeeCategoryProject AmountCurrency Journal ID📎Status
Category → Bexio account mapping
Expense category Tech key Debit acct Debit name Credit acct Credit name Actions
FastAPI backend URL
Azure AD / OIDC authentication
Enable to protect ExpenseHub with Microsoft login. Uses the same app registration as Graph email.
Backend .env also required:
OIDC_ENABLED=true AZURE_TENANT_ID=your-tenant-id AZURE_CLIENT_ID=your-client-id
Azure AD: Add your frontend URL as a Single-page application redirect URI. Also expose an API scope named user_impersonation with App ID URI api://your-client-id.
How to start the backend
# 1. Install dependencies pip install -r requirements.txt # 2. Copy and fill in credentials cp .env.example .env # edit .env with your Cloud SQL host, user, password, db name # 3. Start the server uvicorn main:app --reload --port 8000 # API docs available at: # http://localhost:8000/docs
Email configuration
Used to send rejection notifications and expense reports. Configure one of the two methods below in your .env and restart uvicorn.
Option 1 — SMTP
USE_GRAPH_API=false SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_USER=you@gmail.com SMTP_PASSWORD=app-password SMTP_FROM=you@gmail.com
Works with Gmail, Office 365, or any SMTP server.
Gmail: use an App Password.
Office 365: host smtp.office365.com · port 587
Option 2 — Microsoft Graph API ⭐ recommended
USE_GRAPH_API=true AZURE_TENANT_ID=your-tenant-id AZURE_CLIENT_ID=your-client-id AZURE_CLIENT_SECRET=your-secret AZURE_SENDER_EMAIL=expenses@company.com
No SMTP passwords. Uses Azure AD app registration with Mail.Send permission.
Can reuse the same app registration as OIDC login.
Test email connection
Connection status
To connect, you need a Bexio app with:
Client ID and Client Secret from developer.bexio.com
Redirect URI set to your backend callback URL
Required .env settings
BEXIO_CLIENT_ID=your-client-id BEXIO_CLIENT_SECRET=your-client-secret BEXIO_REDIRECT_URI=http://localhost:8000/api/bexio/callback FRONTEND_URL=http://localhost:5500
OAuth scopes requested
📖 openid — identity
🔄 offline_access — refresh token (stay connected)
📊 accounting — create journal entries
👁 kb_expense_show — read Bexio expenses
✏️ kb_expense_edit — write Bexio expenses
What gets booked
Each expense creates a manual journal entry in Bexio:
Credit: expense account (from your booking rules)
Debit: employee payables account

Book from the Payroll → Paid tab once expenses are paid.
Bexio chart of accounts
Use these account IDs when booking expenses from the Payroll workflow.
Click "Load accounts" to fetch your Bexio chart of accounts.
Test booking
Book paid expenses directly to Bexio. In the normal workflow this is triggered from Payroll → Paid.
Booking log
No bookings yet.
Employee → account suffix
The suffix is appended to the account number when booking to Bexio — e.g. account 2000 + suffix MUELLER2000MUELLER. Leave blank if no suffix is needed.
Employee Email Account suffix Actions
How suffixes are used
When booking an expense to Bexio, the credit account (transfer account) is constructed as:
credit_account = mapping.credit_account_no + user.suffix Example: credit account 2000 + suffix 001 → books to account 2000001.

If no suffix is defined, the base account number is used as-is.
VAT rate → Bexio tax mapping
Map each VAT rate from your expense system to the corresponding tax code in Bexio. The Bexio tax_id will be used when creating manual journal entries.
VAT rate (local) Display name Rate % Bexio tax Bexio code Actions
Bexio taxes
Click "Load Bexio taxes" to fetch available tax rates. Click a row to assign it to a VAT rate.
Not loaded yet.
Local currency → Bexio
Local shortcode Local name Bexio ID Bexio shortcode Bexio name Actions
Bexio currencies
Click "Load Bexio currencies", then click a row to assign it to a local currency.
Not loaded yet.
Templates
Use placeholders to build dynamic strings. The result is truncated to 80 characters as required by Bexio.
Available placeholders
${[ ['{date}', 'Expense date (YYYY-MM-DD)'], ['{journal_id}', 'Journal ID number'], ['{employee_first}', 'First name of employee'], ['{employee_last}', 'Last name of employee'], ['{employee_full}', 'Full name of employee'], ['{category}', 'Expense type name'], ['{amount}', 'Expense amount'], ['{currency}', 'Currency shortcode (CHF, EUR…)'], ].map(([ph, desc]) => ``).join('')}
Placeholder Value
${ph} ${desc}
Live preview
Reference:
Description:
Preview uses sample values: date=today, journal_id=42, employee=Anna Müller, category=Travel
Total spend
Monthly avg
Peak month
Expense types
Monthly spend
By expense type
Breakdown by expense type
Expense type Total Share Distribution
From To
Date Employee Category Project Amount CCY Exch. Exch. CCY VAT TIME 📎 Actions
📎 Receipt Open ↗ ⬇ Download
No attachment
Expense