WorkestraDocs
PlatformTime Tracking

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

RateMeaningUsed forVisibility
hourly_rate (bill)What you charge the client per hourInvoice line items, billable amount in reportsVisible to the entry owner + workspace admins
cost_rate (cost)What the work costs you (loaded employee cost)Profitability tab, margin calculationsWorkspace admins + Time Pro feature gate
currencyISO 4217 (USD / EUR / GBP / …)Currency on invoices, FX conversion at preview timeSnapshotted 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:

  1. Override on (user, project) — most specific
  2. Override on (user, NULL) — applies to that user across all projects
  3. Override on (NULL, project) — applies to that project for any user
  4. Workspace default (time_tracking_settings.default_hourly_rate) — bill only; cost has no workspace-level fallback
  5. 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 written
  • cost_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/newPull from time:

  1. Open the invoice form.
  2. Click Pull from time in the toolbar.
  3. The invoice bridge previews lines grouped by your choice — entry / user / task / project. Each line shows hours × rate = amount, with currency.
  4. Confirm. Lines insert as finance_invoice_line_items rows; the source entries get invoice_line_id set 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_rate for billable + approved
  • CostΣ hours × cost_rate for entries with cost_rate IS NOT NULL
  • ExpensesΣ amount from expenses_expenses rows 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

SurfaceWhat it shows
/time/settings/ratesWorkspace-wide rate-override matrix (bill + cost columns)
ComposerThe bill rate snapshot is shown on hover of the timer entity (informational)
Reports → Detailed tabRate column per row, Amount column = hours × rate
Reports → Profitability tabCost rate × hours = cost; revenue − cost = margin
Invoice bridge previewPer-line rate + currency
CRM deal detail (right rail)Projected cost (cost_rate) + projected margin if deal closes at current value

Public REST API

EndpointWhat
GET /api/v1/time/ratesList overrides for the workspace
POST /api/v1/time/ratesUpsert 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

  • Reports — Profitability tab uses cost rate
  • Approvals — entries become invoice-bridge-eligible only after approval
  • Time Pro — cost rate, retainers, recurring invoicing