Rates & Billing
Bill rate vs. cost rate, override priority, currency snapshotting at write-time, the invoice bridge, retainer pools, and the Profitability tab.
Rates & Billing
Time Tracking maintains two rates per entry — a bill rate (what you charge the client) and a cost rate (what the work costs the company). Both are snapshotted on every time_entries row at write-time so retroactive rate changes never silently rewrite history.
The bill rate feeds invoices. The cost rate feeds profitability. Together they're the basis for margin reporting.
Bill rate vs. cost rate
| Rate | Meaning | Used for | Visibility |
|---|---|---|---|
hourly_rate (bill) | What you charge the client per hour | Invoice line items, billable amount in reports | Visible to the entry owner + workspace admins |
cost_rate (cost) | What the work costs you (loaded employee cost) | Profitability tab, margin calculations | Workspace admins + Time Pro feature gate |
currency | ISO 4217 (USD / EUR / GBP / …) | Currency on invoices, FX conversion at preview time | Snapshotted with both rates |
A new override row in time_rate_overrides can set both, just bill, or just cost. Whatever's set propagates onto every new entry the resolver picks up.
Override priority
When the rate resolver computes the rate for (user, project, log_date), it walks priorities in order, picks the first match:
- Override on
(user, project)— most specific - Override on
(user, NULL)— applies to that user across all projects - Override on
(NULL, project)— applies to that project for any user - Workspace default (
time_tracking_settings.default_hourly_rate) — bill only; cost has no workspace-level fallback - Zero — no override + no default → 0
Within a bucket, the most recent applicable effective_from wins. An override applies on asOfDate if effective_from <= asOfDate AND (effective_to IS NULL OR asOfDate <= effective_to).
This means you can:
- Bump everyone's rate by setting
(NULL, NULL, $200, effective 2026-04-01)— applies to anyone on any project starting Apr 1. - Override one consultant on one client by setting
(alice, acme, $250, …)— wins over both her general rate and Acme's project rate. - Open-ended date ranges (
effective_to = NULL) for "current rates"; close them with a date when you raise.
Snapshotting at write-time
Every time_entries row carries:
hourly_rate— bill rate at the time the entry was writtencost_rate— cost rate at the time the entry was written (nullable; null means "unknown")currency— currency at the time of write
These are immutable post-creation. Editing a rate override only affects future entries — historical reporting / billing stays correct.
When you start a timer, the snapshot happens at start. When you log a manual entry, at create. When the calendar import bridge converts a meeting, at convert.
The invoice bridge
Approved + billable + un-invoiced entries feed /finance/invoices/new → Pull from time:
- Open the invoice form.
- Click Pull from time in the toolbar.
- The invoice bridge previews lines grouped by your choice — entry / user / task / project. Each line shows hours × rate = amount, with currency.
- Confirm. Lines insert as
finance_invoice_line_itemsrows; the source entries getinvoice_line_idset so they don't show up in the next preview.
Currency consistency
The bridge rejects mixed-currency sets. If you select entries in USD and EUR, you get an error: pull each currency into its own invoice (or convert manually first). FX conversion in v1 is intentionally out of scope — too easy to introduce subtle bugs.
When all entries share a single currency, conversion is just sum(hours × rate) per group.
Voiding
Voiding an invoice (or deleting an invoice line) sets time_entries.invoice_line_id = NULL for the affected rows. They re-enter the un-invoiced pool and show up in the next preview.
Profitability (Pro)
The Profitability tab at /time/reports?tab=profitability is where bill rate × cost rate pays off. Per-project rows:
- Revenue —
Σ hours × hourly_ratefor billable + approved - Cost —
Σ hours × cost_ratefor entries withcost_rate IS NOT NULL - Expenses —
Σ amountfromexpenses_expensesrows tagged with the project - Margin = Revenue − Cost − Expenses
- Margin % = Margin / Revenue
Sorted by margin desc by default. Surfaces unprofitable projects immediately; a warning chip flags entries missing cost_rate (excluded from cost).
Recurring invoicing from time (Pro)
A finance_recurring_invoices row with source = 'time' automates the bridge: every period (weekly / monthly / quarterly), the cron sweeps approved + billable + un-invoiced hours into a fresh invoice draft. Configure at /finance/settings/recurring.
Retainer pools (Pro)
finance_retainers carries a contact's retainer balance: (hours_total, hours_used, period). When a time_entry on that contact is approved + billable, a hook decrements hours_used. The current balance is shown on the contact's right-rail, and on every new entry the composer shows "Acme: 7.5 / 20 retainer hours used this month".
When hours_used >= hours_total, new entries trip a soft warning ("Acme is over retainer — confirm to log"). Hard-block is configurable per retainer.
FX rates (future)
Mixed-currency invoicing isn't supported in v1. The roadmap includes:
- Daily ECB FX rates (table
fx_rates) - Per-line FX snapshot on
finance_invoice_line_items - Multi-currency invoice support with conversion at line creation time
Until then: pull each currency into its own invoice.
Where rates show up
| Surface | What it shows |
|---|---|
/time/settings/rates | Workspace-wide rate-override matrix (bill + cost columns) |
| Composer | The bill rate snapshot is shown on hover of the timer entity (informational) |
| Reports → Detailed tab | Rate column per row, Amount column = hours × rate |
| Reports → Profitability tab | Cost rate × hours = cost; revenue − cost = margin |
| Invoice bridge preview | Per-line rate + currency |
| CRM deal detail (right rail) | Projected cost (cost_rate) + projected margin if deal closes at current value |
Public REST API
| Endpoint | What |
|---|---|
GET /api/v1/time/rates | List overrides for the workspace |
POST /api/v1/time/rates | Upsert an override (create on no id, update on id) |
DELETE /api/v1/time/rates/[id] | Delete an override |
Bearer-auth scoped to the workspace.
Next steps
Approvals
Multi-level approval chains, period locking after final approval, notifications at every step. Single-level fallback for free workspaces.
Reports
Five tabs over the same dataset — Summary, Detailed, Weekly pivot, Profitability, and saved My Reports. Toggl-style filter row + toolbar across every tab.